בונים אפליקציה שסופרת נקודות במשחק בין שתי קבוצות. בדרך לומדים איך מסדרים רכיבים על המסך: מה למעלה, מה למטה, ומה זה ליד זה.
הספורט, שמות הקבוצות והצבעים הם בחירה שלכם: כדורסל, כדורגל, פינג-פונג, קלפים. המדריך כתוב על כדורסל (מאפשר הוספה של 3 נק', 2 נק', או 1 נק').
| חלק | מה עושים | נקודות | זמן משוער |
|---|---|---|---|
| חזרה | פרויקט חדש בלי מדריך, מהזיכרון | 10 | 20 דק' |
| הפרויקט | לוח תוצאות עובד לשתי קבוצות, עם איפוס | 40 | 45 דק' |
| נקודת בדיקה | מסבירים למורה שלושה דברים קצרים | 10 | 10 דק' |
| הרחבה 1 | עיצוב הקבוצות | 10 | בזמן שנשאר ובבית |
| הרחבה 2 | כפתור תיקון טעות | 15 | |
| הרחבה 3 | סיום משחק והכרזת מנצח | 15 | |
| מסלול מתקדם | שלוש משימות | בונוס עד 30 |
ניקוד על הרחבות ועל המסלול המתקדם נרשם רק אחרי שעברתם את נקודת הבדיקה מול המורה. אפליקציה שעובדת בלי שאתם יודעים להסביר אותה לא נחשבת.
ScoreBoard. השם באנגלית בלבד, בלי רווחים ובלי תווים מיוחדים. תבנית Empty Views Activity, שפה Java, Minimum SDK API 26, Build configuration language Groovy DSL.activity_main.xml במצב Code, מחקו את כל התוכן שלו והדביקו במקומו את שלד ה-LinearLayout שמופיע מתחת לרשימה.TextView: id בשם tvTitle, טקסט עם שם המשחק שלכם, וגודל טקסט 28sp.btnWhistle וטקסט "שריקת פתיחה". לחיצה עליו מציגה Toast: "המשחק התחיל!".colors.xml הוסיפו צבע חדש בשם screen_bg (צבע בהיר לבחירתכם), והגדירו אותו כרקע של המסך.במסך בחירת התבנית יש שתי תבניות עם שם כמעט זהה, זו ליד זו. הנכונה היא Empty Views Activity, עם המילה Views.
איך מזהים שבחרתם לא נכון: במסך הבא אין שדה Language, ומתחת לכותרת כתוב "Jetpack Compose". במקרה כזה לחצו Previous, בחרו Empty Views Activity, ורק אז בחרו Java.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- את הרכיבים שלכם מוסיפים כאן, בין שתי התגיות -->
</LinearLayout>פרויקט חדש נפתח עם קופסה מסוג ConstraintLayout. בקופסה כזו כל רכיב חייב להיות "קשור" לשכנים שלו, ורכיב שלא נקשר מסומן בשגיאה אדומה וקופץ לפינת המסך. זו הסיבה לרוב התקלות בתחילת הדרך.
ב-LinearLayout אין צורך לקשור כלום: כל רכיב שמוסיפים מופיע מתחת לקודם. מה בדיוק כתוב בשלד תלמדו בחלק 2.
android:id="@+id/main"ב-MainActivity.java יש שורה ש-Android Studio כתב בשבילכם ומחפשת רכיב בשם main. אם ה-id הזה חסר, תקבלו שגיאה אדומה על R.id.main והאפליקציה לא תיבנה.
בשלב הזה הרכיבים יופיעו אחד מתחת לשני, צמודים לצד המסך. את היישור למרכז נעשה בחלק 3. חשוב לדייק בשמות (tvTitle, btnWhistle, screen_bg) כי ההמשך משתמש בהם.
פתחו את app > res > layout > activity_main.xml. בפינה הימנית העליונה של העורך יש שלושה מצבים: Code, Split, Design. עברו ל-Code. רכיב חדש כותבים בתוך השלד, במקום שורת ההערה. כך נראה TextView עם id:
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="גמר הליגה"
android:textSize="28sp" />כפתור נכתב באותה צורה, עם <Button במקום <TextView. מספיק להקליד <Bu ולבחור מההשלמה האוטומטית.
הקובץ נמצא ב-app > res > values > colors.xml. מוסיפים שורה לפני התגית הסוגרת </resources>:
<color name="screen_bg">#FFF8EE</color>
ובתגית הפותחת של ה-LinearLayout ב-activity_main.xml, כשורה נוספת:
android:background="@color/screen_bg"
ב-MainActivity.java, בתוך onCreate, אחרי השורות שכבר כתובות שם:
Button btnWhistle = findViewById(R.id.btnWhistle);
btnWhistle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "המשחק התחיל!", Toast.LENGTH_SHORT).show();
}
});אם Button, View או Toast מסומנים באדום, עמדו על המילה ולחצו Alt+Enter ואז Import class.
שימו לב שכתוב MainActivity.this ולא this: בתוך ה-OnClickListener המילה this לבדה מצביעה על המאזין עצמו ולא על המסך.
האפליקציה רצה, רואים את שם המשחק על רקע בצבע שבחרתם, ולחיצה על הכפתור מציגה את ההודעה. קראו למורה לרישום 10 הנקודות.
בחלק 1 הדבקתם שלד של LinearLayout בלי לדעת מה הוא. עכשיו מבינים אותו. עד עכשיו שמתם רכיבים על המסך בלי לחשוב איפה הם יושבים. באפליקציה אמיתית צריך להחליט: מה למעלה, מה למטה, מה זה ליד זה. מי שמחליט על כך הוא ה-Layout.
Layout הוא קופסה שקופה שמחזיקה בתוכה רכיבים ומסדרת אותם לפי חוק. לקופסה מהסוג LinearLayout יש חוק פשוט מאוד: היא מסדרת את הרכיבים שבתוכה בקו אחד. או אחד מתחת לשני (vertical), או אחד ליד השני (horizontal). כמו מדף ספרים: ספרים עומדים בשורה, או מונחים בערימה.
orientation="vertical"orientation="horizontal"מה עושים כשרוצים גם וגם? כותרת למעלה, מתחתיה שתי קבוצות זו ליד זו, ולמטה כפתור. הפתרון: שמים LinearLayout בתוך LinearLayout. לזה קוראים קינון. כך בנוי המסך שלנו:
| מושג | מה הוא אומר | דימוי |
|---|---|---|
match_parent | תהיה גדול כמו הקופסה שמחזיקה אותך | שטיח מקיר לקיר |
wrap_content | תהיה גדול רק כמו התוכן שלך | חולצה במידה שלך |
layout_weight | איך מחלקים את המקום הפנוי בין הרכיבים. משקל שווה, חלק שווה | חלוקת פיצה: לכל אחד 1, כולם מקבלים אותו דבר |
padding ו-margin | padding הוא רווח בתוך הרכיב, בין הגבול לתוכן. margin הוא רווח מחוץ לרכיב, בינו לבין השכנים | padding: ריפוד בתוך קופסה. margin: מרחק בין שתי קופסאות |
gravity קובע איפה התוכן יושב בתוך הרכיב (למשל טקסט ממורכז בתוך TextView). layout_gravity קובע איפה הרכיב עצמו יושב בתוך הקופסה שמחזיקה אותו. תפגשו את שניהם בהמשך.
עובדים בשני קבצים: activity_main.xml (איך המסך נראה) ו-MainActivity.java (מה קורה כשלוחצים). אחרי כל תיבה כחולה של "הריצו ובדקו" מריצים ובודקים. לא ממשיכים הלאה עם שגיאה.
בעץ הקבצים משמאל: app > res > layout > activity_main.xml. לחיצה כפולה. בפינה הימנית העליונה של העורך בחרו Code (או Split אם אתם רוצים לראות תצוגה מקדימה ליד הקוד).
הקובץ שלכם כבר מכיל LinearLayout עם כותרת וכפתור מחלק 1. עדכנו אותו כך שייראה כמו הקוד הבא. שלוש שורות חדשות: android:gravity="center" בכותרת, ו-android:layout_gravity="center_horizontal" ו-android:layout_marginTop="8dp" בכפתור. ודאו גם שהרוחב של הכותרת הוא match_parent ושל הכפתור wrap_content.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/screen_bg"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="גמר הליגה"
android:textSize="28sp" />
<Button
android:id="@+id/btnWhistle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:text="שריקת פתיחה" />
</LinearLayout>orientation="vertical": הרכיבים מסודרים אחד מתחת לשני. padding="16dp": רווח פנימי של 16 מכל הצדדים, כדי ששום דבר לא יידבק לקצה המסך.
בכותרת: הרוחב match_parent (כל רוחב המסך) ו-gravity="center" ממרכז את הטקסט בתוכה.
בכפתור: הרוחב wrap_content (רק כמו הטקסט), ולכן ממרכזים את הכפתור עצמו עם layout_gravity.
הכותרת למעלה באמצע, הכפתור מתחתיה באמצע, והוא עדיין מציג את ה-Toast. קו צהוב מתחת לטקסט בעברית הוא רק אזהרה, אפשר להתעלם ממנו בינתיים.
הדביקו את הקוד הבא אחרי התגית של הכפתור (/>) ולפני השורה האחרונה </LinearLayout>.
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#FFE0B2"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="8dp">
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#B2DFDB"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="8dp">
</LinearLayout>
</LinearLayout>השורה האופקית: גובה 0dp יחד עם layout_weight="1" פירושו "קח את כל הגובה שנשאר במסך".
כל עמודה: רוחב 0dp יחד עם layout_weight="1". שתיהן עם אותו משקל, ולכן כל אחת מקבלת בדיוק חצי מהרוחב. זו הפיצה מההסבר.
שני הצבעים (#FFE0B2, #B2DFDB) זמניים. הם שם כדי שתראו בעיניים איפה כל עמודה נמצאת.
מתחת לכפתור רואים שני מלבנים צבעוניים שממלאים את שאר המסך, חצי-חצי.
ניסוי של דקה: בעמודה הראשונה שנו את layout_weight מ-1 ל-2 והריצו. מה קרה לרוחב? החזירו ל-1.
במכשיר שמוגדר בעברית, שורה אופקית מסתדרת מימין לשמאל, ולכן העמודה הראשונה בקוד תופיע מימין. במכשיר באנגלית היא תופיע משמאל. זה תקין.
מצאו את העמודה הראשונה (זו עם הרקע #FFE0B2). בין התגית הפותחת שלה לבין </LinearLayout> שסוגר אותה יש שורה ריקה. הדביקו שם את הקוד הבא, ושנו את "הנשרים" לשם הקבוצה שלכם.
<TextView
android:id="@+id/tvNameA"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="הנשרים"
android:textSize="20sp" />
<TextView
android:id="@+id/tvScoreA"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="0"
android:textSize="64sp" />
<Button
android:id="@+id/btnA3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="+3" />
<Button
android:id="@+id/btnA2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="+2" />
<Button
android:id="@+id/btnA1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="+1" />העמודה היא LinearLayout אנכי, ולכן חמשת הרכיבים מסתדרים אחד מתחת לשני. ה-gravity="center_horizontal" שכתבנו על העמודה בצעד 3 ממרכז את השם ואת הניקוד.
לניקוד יש layout_marginTop ו-layout_marginBottom: רווח חיצוני מעליו ומתחתיו, כדי שלא יידבק לשם ולכפתורים.
הכפתורים ברוחב match_parent: כל רוחב העמודה (לא כל רוחב המסך, כי ה"הורה" שלהם הוא העמודה).
בעמודה אחת רואים שם קבוצה, 0 גדול ושלושה כפתורים. הכפתורים עדיין לא עושים כלום.
פתחו את app > java > (שם החבילה שלכם) > MainActivity.
חלק ראשון: מצאו את השורה public class MainActivity extends AppCompatActivity {. מיד אחריה, לפני @Override, הוסיפו:
int scoreA = 0;
TextView tvScoreA;חלק שני: בתוך onCreate, אחרי הקוד של כפתור השריקה ולפני הסוגר המסולסל } שסוגר את onCreate, הוסיפו:
tvScoreA = findViewById(R.id.tvScoreA);
Button btnA3 = findViewById(R.id.btnA3);
Button btnA2 = findViewById(R.id.btnA2);
Button btnA1 = findViewById(R.id.btnA1);
btnA3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scoreA = scoreA + 3;
tvScoreA.setText(String.valueOf(scoreA));
}
});
btnA2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scoreA = scoreA + 2;
tvScoreA.setText(String.valueOf(scoreA));
}
});
btnA1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scoreA = scoreA + 1;
tvScoreA.setText(String.valueOf(scoreA));
}
});אם TextView או View מסומנים באדום: עמדו על המילה, Alt+Enter, Import class.
scoreA הוא משתנה שזוכר את הניקוד. הוא מוגדר בראש המחלקה ולא בתוך onCreate, כדי שכל הכפתורים יוכלו לשנות אותו והוא לא "יישכח" בין לחיצה ללחיצה.
findViewById מחבר בין רכיב שציירנו ב-XML (לפי ה-id שלו) לבין משתנה ב-Java.
setOnClickListener מצמיד לכפתור "מאזין". מה שכתוב בתוך הפעולה onClick הוא מה שירוץ בכל לחיצה. כשמקלידים new View.On ובוחרים מההשלמה האוטומטית, Android Studio כותב את כל המעטפת בשבילכם.
בכל לחיצה קורים שני דברים: מעדכנים את המספר בזיכרון, ואז מציגים אותו על המסך. String.valueOf הופך את המספר לטקסט, כי setText צריך לקבל טקסט.
לחצו +3, +2, +1. הניקוד צריך להיות 6.
העמודה השנייה היא העתק של הראשונה עם שמות אחרים. בצעו לבד:
#B2DFDB).scoreB ו-tvScoreB בראש המחלקה, ואת אותו קוד של הכפתורים עם B במקום A.| בקבוצה א' | בקבוצה ב' |
|---|---|
tvNameA | tvNameB |
tvScoreA | tvScoreB |
btnA3, btnA2, btnA1 | btnB3, btnB2, btnB1 |
scoreA | scoreB |
שוכחים לשנות A ל-B באחד המקומות. אם לחיצה על כפתור של קבוצה ב' מעלה את הניקוד של קבוצה א', חפשו בקוד של קבוצה ב' איפה נשאר A.
בראש המחלקה:
int scoreB = 0;
TextView tvScoreB;בסוף onCreate:
tvScoreB = findViewById(R.id.tvScoreB);
Button btnB3 = findViewById(R.id.btnB3);
Button btnB2 = findViewById(R.id.btnB2);
Button btnB1 = findViewById(R.id.btnB1);
btnB3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scoreB = scoreB + 3;
tvScoreB.setText(String.valueOf(scoreB));
}
});
btnB2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scoreB = scoreB + 2;
tvScoreB.setText(String.valueOf(scoreB));
}
});
btnB1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scoreB = scoreB + 1;
tvScoreB.setText(String.valueOf(scoreB));
}
});כל קבוצה סופרת בנפרד. לחיצות על קבוצה ב' לא משנות את הניקוד של קבוצה א'.
צבע: ב-colors.xml הוסיפו לפני </resources>:
<color name="btn_reset">#C62828</color>
הכפתור: ב-activity_main.xml, אחרי ה-</LinearLayout> שסוגר את השורה האופקית ולפני ה-</LinearLayout> האחרון בקובץ:
<Button
android:id="@+id/btnReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:backgroundTint="@color/btn_reset"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="איפוס"
android:textColor="#FFFFFF"
android:textSize="18sp" />הקוד: בסוף onCreate ב-MainActivity.java:
Button btnReset = findViewById(R.id.btnReset);
btnReset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scoreA = 0;
scoreB = 0;
tvScoreA.setText("0");
tvScoreB.setText("0");
}
});scoreB או tvScoreB מסומנים באדום?
אל תמחקו את השורה ואל תמשיכו הלאה. זו תקלה שכדאי להבין לבד:
Cannot resolve symbol, כלומר "אני לא מכיר את השם הזה".scoreA לא אדום ו-scoreB כן? מה נעשה ל-scoreA שלא נעשה ל-scoreB?אין הגדרה של המשתנה. ב-Java אי אפשר להשתמש במשתנה לפני שהגדירו אותו. חפשו בראש המחלקה איפה מוגדרים scoreA ו-tvScoreA, והשלימו לידם את מה שחסר לקבוצה ב'. זה חלק מצעד 6.
| מאפיין | מה הוא קובע |
|---|---|
android:text | הטקסט שעל הכפתור |
android:textSize | גודל הטקסט, ביחידות sp |
android:textColor | צבע הטקסט |
android:backgroundTint | צבע הרקע של הכפתור |
android:paddingStart / paddingEnd | רווח פנימי בצדדים, מרחיב את הכפתור |
android:enabled | false הופך את הכפתור לאפור ולא לחיץ |
בפרויקטים חדשים הכפתור מגיע עם עיצוב מוכן (Material). אם תכתבו android:background עם צבע, ברוב המקרים לא יקרה כלום. הדרך לשנות צבע של כפתור היא backgroundTint.
קראו למורה. תתבקשו להסביר בעל-פה את שלושת הדברים הבאים. הכינו את התשובות לפני כן, ונסו כל דבר באפליקציה שלכם.
תלמיד כתב את הקוד הבא. האפליקציה נבנית בלי שגיאות, אבל קורסת ברגע שלוחצים על הכפתור.
btnA1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scoreA = scoreA + 1;
tvScoreA.setText(scoreA);
}
});החליפו זמנית את הקוד שלכם בקוד הזה, הריצו ולחצו. פתחו את חלון Logcat בתחתית Android Studio וחפשו את השורות האדומות. מה שם השגיאה? מה לא בסדר בקוד, ואיך מתקנים? אל תשכחו להחזיר את הקוד התקין.
נחשו קודם, ורק אחר כך נסו:
android:layout_weight="1" מהעמודה של קבוצה ב' בלבד. מה רואים על המסך, ולמה?layout_weight של עמודה א' ל-3 ומשאירים את ב' על 1. איזה חלק מרוחב המסך מקבלת כל קבוצה?מה ההבדל בין gravity ל-layout_gravity? הראו בקוד שלכם מקום אחד שבו השתמשנו בכל אחד מהם.
כאן אין צעד-צעד. יש דרישה, הגדרת הצלחה ורמז. כל מה שצריך כבר מופיע במדריך הזה או בשני המדריכים הקודמים.
כרגע צבעי העמודות כתובים ישירות בקוד וכל הכפתורים נראים אותו דבר. תנו לכל קבוצה 'זהות':
colors.xml (שמות לדוגמה: team_a_bg, team_b_bg), בלי קוד צבע ישיר ב-layout.textStyle).8dp, כך שרואים את רקע המסך ביניהן.הצלחה: אין אף # בקובץ activity_main.xml, והמסך נראה כמו לוח של שתי קבוצות שונות.
רווח בין שתי קופסאות שכנות הוא margin. נסו layout_marginEnd על העמודה הראשונה.
השופט לחץ בטעות. הוסיפו לכל קבוצה כפתור "-1" שמוריד נקודה אחת.
הצלחה: מ-2 לוחצים -1 שלוש פעמים. רואים 1, אחר כך 0, ובפעם השלישית נשאר 0 ומופיעה ההודעה.
לפני שמורידים, בודקים עם if אם הניקוד גדול מ-0.
הוסיפו כפתור "סיום משחק" שיושב ליד כפתור האיפוס, באותה שורה.
הצלחה: אחרי "סיום משחק" אי אפשר לשנות את הניקוד עד שלוחצים "איפוס".
שני כפתורים זה ליד זה, בתוך מסך אנכי: צריך קופסה חדשה. וב-Java, לכל כפתור יש פעולה setEnabled(false).
פתוח לכל מי שסיים את הפרויקט ואת נקודת הבדיקה. כאן אין צעדים ואין רמזים. יש יעד וקישור לתיעוד הרשמי, ואתם לומדים לבד. על כל משימה תצטרכו להסביר למורה מה עשיתם ולמה זה עובד.
קינון של קופסה בתוך קופסה עובד, אבל במסכים מורכבים הוא נהיה מסורבל ואיטי. ConstraintLayout פותר את זה: כל הרכיבים יושבים בקופסה אחת, וכל רכיב "נקשר" לשכנים שלו.
activity_main_constraint.xml ובנו בו את אותו מסך בדיוק, בלי אף LinearLayout.Guideline ב-50% או בעזרת chain.setContentView וודאו שהקוד ממשיך לעבוד בלי שינוי נוסף.למורה: מתי הייתם בוחרים ב-LinearLayout ומתי ב-ConstraintLayout?
צברו כמה נקודות וסובבו את המכשיר לרוחב. תגלו שתי בעיות: חלק מהמסך נחתך, והניקוד חוזר ל-0.
layout-land) עם סידור שמתאים למסך רחב ונמוך, כך ששום דבר לא נחתך.onSaveInstanceState.למורה: מה בדיוק קורה ל-Activity ברגע הסיבוב? (זה הנושא של המדריך הבא, אתם מקדימים את הכיתה.)
בפתרון הבסיסי יש שישה כפתורים כמעט זהים ב-XML ושישה קטעי קוד כמעט זהים ב-Java. מתכנת מקצועי לא משאיר את זה כך.
style אחד לכפתורי הניקוד, וכל כפתור רק מפנה אליו.strings.xml, עד שלא נשארת אף אזהרה צהובה ב-layout.למורה: אם מחר נרצה להוסיף קבוצה שלישית, כמה שורות צריך לשנות אצלכם, וכמה בפתרון הבסיסי?
סמנו מה שהמורה אישר. הסימון נשמר בדפדפן שלכם בלבד.
הראו למורה את האפליקציה רצה, ואז הגישו כרגיל: File ← Export ← Export to Zip File, והעלאה ל-Classroom.