בסוף הפרק נראה לוח בן 6 שורות ו־7 עמודות. נגיעה בלוח תציג את מספר העמודה שנבחרה. עדיין אין דיסקיות או תורות.
נקודת ההתחלה היא Empty Views Activity בשפת Java, עם View Binding פעיל: ActivityMainBinding.inflate, ואז setContentView(binding.getRoot()). אין כאן שלב יצירת פרויקט. הקוד מניח חבילה com.example.connect4.
ה־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.