בסוף הפרק נראה לוח בן 6 שורות ו־7 עמודות. נגיעה בלוח תציג את מספר העמודה שנבחרה. עדיין אין דיסקיות או תורות.
התחילו בפרויקט Empty Views Activity בשפת Java, חבילה com.example.connect4 ו־XML. לפני בניית הלוח, השלימו את הסבת הפרויקט ל־View Binding בסעיף הבא; היא מכינה את MainActivity ואת ה־layout שבהם נשתמש בפרק.
הסבת הפרויקט ל־View Binding
View Binding יוצר מחלקה עם הפניות ישירות ל־Views שבקובץ ה־XML, כך שלא צריך לחפש כל רכיב שוב באמצעות findViewById. כך הקוד קצר יותר, ושגיאות של מזהה או המרה בין טיפוסים מתגלות מוקדם יותר.
שלב 1 — הפעלת View Binding ב־Gradle
פתחו את build.gradle.kts (Module :app) והוסיפו את buildFeatures:
android {
namespace = "com.example.connect4"
compileSdk {
version = release(36)
}
+ buildFeatures {
+ viewBinding = true
+ }
+
defaultConfig {
...
}
}
לאחר העריכה סנכרנו את ה-Gradle .
שלב 2 — המרת MainActivity לשימוש ב־Binding
עִרְכוּ את MainActivity.java:
לפני
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
}אחרי
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
}השדה binding מופיע באדום. בצעו right-click > ShowContextActions ובחרו Create Field:
זה יוסיף את השדה וגם יוסיף את ה-import שחסר לנו.
הסבר קצר: binding.getRoot() הוא ה־View הראשי של הפריסה. ב־binding.main ניגשים ל־View עם android:id="@+id/main"; לכן אין עוד צורך ב־findViewById. קוד ה־insets והריווח נשאר ללא שינוי.
ה־Activity אחראי לתגובה; ה־View אחראי לציור ולתרגום נקודת המגע לעמודה. הממשק ColumnListener מחבר ביניהם בלי שהלוח צריך להכיר את ה־Activity.
השינויים לפי סדר העבודה
התחילו במצב שעבד בסוף הפרק הקודם. קובץ חדש מוצג במלואו; בקובץ קיים מופיעים רק האזורים שמשתנים. בקטעי diff מסירים את שורות -, מוסיפים את שורות +, ומשאירים את שורות ההקשר. הסימנים עצמם אינם חלק מקוד Java או XML. אין למחוק קוד אחר שאינו מוצג.
activity_main.xml
app > res > layout > activity_main.xml
פתחו את התצוגה ב־Code. שומרים את ConstraintLayout של התבנית. נותנים מזהה להודעה, מסירים רק את העיגון התחתון שלה, ומוסיפים את הלוח מתחתיה. היחס 7:6 שומר על תאים ריבועיים. את BoardView ניצור מיד בשלב הבא; שגיאת התצוגה הזמנית תיעלם לאחר יצירת המחלקה.
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Hello World!"
- app:layout_constraintBottom_toBottomOf="parent"
+ android:id="@+id/status"
+ android:text="Tap a column"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+ <com.example.connect4.ui.BoardView
+ android:id="@+id/board"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintDimensionRatio="7:6"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/status" />
+
</androidx.constraintlayout.widget.ConstraintLayout>
BoardView.java
app > kotlin+java > com.example.connect4 > ui > BoardView.java
צרו package בשם ui, ובו Java Class בשם BoardView. הבנאי עם AttributeSet מאפשר ל־Android ליצור את המחלקה מתוך XML. משתמשים באותו cellSize() לציור ולמגע: אם יש שטח נוסף ב־View, הוא אינו נחשב לחלק מהלוח.
מרכז תא הוא (column + 0.5) * size בציר האופקי ו־(row + 0.5) * size בציר האנכי. ב־Canvas הראשית בפינה השמאלית העליונה. ACTION_UP יוצר בחירה אחת בשחרור האצבע; performClick() משמר את חוזה הלחיצה של View.
package com.example.connect4.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
/** Draws a six-by-seven board and reports column taps to its controller. */
public final class BoardView extends View {
private static final int ROWS = 6;
private static final int COLUMNS = 7;
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private ColumnListener listener;
/** Receives a zero-based column; the View does not apply game rules. */
public interface ColumnListener { void onColumn(int column); }
/** Called by Android when the custom View is inflated from XML. */
public BoardView(Context context, AttributeSet attrs) {
super(context, attrs);
setContentDescription("Connect Four board. Tap a column to drop a disc.");
}
/** Connects the Activity's response to a board tap. */
public void setColumnListener(ColumnListener listener) { this.listener = listener; }
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float size = cellSize();
paint.setColor(0xff1565c0);
canvas.drawRect(0, 0, size * COLUMNS, size * ROWS, paint);
for (int row = 0; row < ROWS; row++) {
for (int column = 0; column < COLUMNS; column++) {
paint.setColor(0xffeeeeee);
canvas.drawCircle((column + .5f) * size, (row + .5f) * size,
size * .38f, paint);
}
}
}
/** Uses the same geometry for drawing and hit testing. */
private float cellSize() {
return Math.min(getWidth() / (float) COLUMNS, getHeight() / (float) ROWS);
}
@Override public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
float size = cellSize();
float x = event.getX(), y = event.getY();
if (size > 0 && x >= 0 && x < size * COLUMNS && y >= 0 && y < size * ROWS) {
performClick();
if (listener != null) listener.onColumn((int) (x / size));
}
}
return true;
}
@Override public boolean performClick() { super.performClick(); return true; }
}
MainActivity.java
app > kotlin+java > com.example.connect4 > MainActivity.java
רשמו listener אחרי הגדרת ה־insets ב־onCreate. הקוד מקבל עמודה בטווח 0–6; רק הכיתוב לתלמיד מציג 1–7. ההמרה היא להצגה בלבד.
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
+ binding.board.setColumnListener(column ->
+ binding.status.setText("Column " + (column + 1)));
}
}
מריצים ובודקים
- בצעו Build > Make Project, ואז Run.
- געו במרכז התא השמאלי: ההודעה צריכה להיות
Column 1. בקצה הימני:Column 7. - געו מעל הלוח או בשטח מחוצה לו: הבחירה אינה משתנה.
- סובבו את המכשיר: הלוח נשאר ביחס 7:6. עדיין לא שומרים בחירה בסיבוב.
המורה בדק גם View ברוחב 700 וגובה 300: הלוח המצויר מסתיים ב־x=350, ולכן x=349 הוא עמודה 6 ואילו x=350 כבר מחוץ ללוח.
בדיקת הבנה
מדוע חלוקה ב־getWidth()/7 בלבד עלולה לבחור עמודה שגויה כאשר ה־View גבוה או רחב מהלוח המצויר? חשבו את העמודה עבור x=125 כאשר גודל התא הוא 50.