בשיעור הזה נסביר איך לעקוב אחרי תנועה באירועי מגע.
אירוע חדש של onTouchEvent() מופעל עם אירוע של ACTION_MOVE בכל פעם שמשתנים המיקום, הלחץ או הגודל של נקודת המגע הנוכחית. כמו שמתואר במאמר בנושא זיהוי תנועות נפוצות, כל האירועים האלה מתועדים בפרמטר MotionEvent של onTouchEvent().
מכיוון שמגע באמצעות האצבעות הוא לא תמיד הצורה המדויקת ביותר של אינטראקציה, זיהוי אירועי מגע מבוסס לרוב יותר על תנועה מאשר על מגע פשוט. כדי לעזור לאפליקציות להבחין בין תנועות שמבוססות על תזוזה (כמו החלקה) לבין תנועות שלא מבוססות על תזוזה (כמו הקשה בודדת), מערכת Android כוללת את המושג סבילות למגע. המונח 'סובלנות לסטייה במגע' מתייחס למרחק בפיקסלים שהמגע של המשתמש יכול לסטות לפני שהתנועה תפורש כמחווה שמבוססת על תנועה. מידע נוסף על הנושא הזה זמין במאמר ניהול אירועי מגע ב-ViewGroup.
יש כמה דרכים לעקוב אחרי תנועה במחוות, בהתאם לצרכים של האפליקציה. הנה כמה דוגמאות:
- המיקום ההתחלתי והסופי של מצביע, למשל הזזה של אובייקט במסך מנקודה א' לנקודה ב'.
- הכיוון שבו מצביע העכבר נע, כפי שנקבע על ידי הקואורדינטות X ו-Y.
- היסטוריה. כדי לדעת מה הגודל של היסטוריית תנועות, מפעילים את השיטה
MotionEventgetHistorySize(). לאחר מכן, אפשר לקבל את המיקומים, הגדלים, הזמן והלחצים של כל אחד מהאירועים ההיסטוריים באמצעות השיטות של אירוע התנועהgetHistorical<Value>. ההיסטוריה שימושית כשמציגים שוב את התנועה של האצבע של המשתמש, למשל כשמציירים באמצעות מגע. פרטים נוספים מופיעים במאמר בנושאMotionEvent. - המהירות של הסמן כשהוא נע על מסך המגע.
כדאי לעיין במקורות המידע הבאים שקשורים לנושא:
מהירות הטראק
אפשר להגדיר תנועה כמחווה על סמך המרחק או הכיוון של תנועת הסמן. עם זאת, המהירות היא לרוב גורם מכריע במעקב אחרי מאפיינים של תנועה או בקביעה אם התנועה התרחשה. כדי להקל על חישוב המהירות, מערכת Android מספקת את המחלקה VelocityTracker.
VelocityTracker עוזר לעקוב אחרי המהירות של אירועי מגע. ההגדרה הזו שימושית לתנועות שבהן המהירות היא חלק מהקריטריונים לתנועה, כמו תנועת החלקה.
דוגמה שממחישה את המטרה של המתודות ב-API VelocityTracker:
Kotlin
private const val DEBUG_TAG = "Velocity" class MainActivity : Activity() { private var mVelocityTracker: VelocityTracker? = null override fun onTouchEvent(event: MotionEvent): Boolean { when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { // Reset the velocity tracker back to its initial state. mVelocityTracker?.clear() // If necessary, retrieve a new VelocityTracker object to watch // the velocity of a motion. mVelocityTracker = mVelocityTracker ?: VelocityTracker.obtain() // Add a user's movement to the tracker. mVelocityTracker?.addMovement(event) } MotionEvent.ACTION_MOVE -> { mVelocityTracker?.apply { val pointerId: Int = event.getPointerId(event.actionIndex) addMovement(event) // When you want to determine the velocity, call // computeCurrentVelocity(). Then, call getXVelocity() and // getYVelocity() to retrieve the velocity for each pointer // ID. computeCurrentVelocity(1000) // Log velocity of pixels per second. It's best practice to // use VelocityTrackerCompat where possible. Log.d("", "X velocity: ${getXVelocity(pointerId)}") Log.d("", "Y velocity: ${getYVelocity(pointerId)}") } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { // Return a VelocityTracker object back to be re-used by others. mVelocityTracker?.recycle() mVelocityTracker = null } } return true } }
Java
public class MainActivity extends Activity { private static final String DEBUG_TAG = "Velocity"; ... private VelocityTracker mVelocityTracker = null; @Override public boolean onTouchEvent(MotionEvent event) { int index = event.getActionIndex(); int action = event.getActionMasked(); int pointerId = event.getPointerId(index); switch(action) { case MotionEvent.ACTION_DOWN: if(mVelocityTracker == null) { // Retrieve a new VelocityTracker object to watch the // velocity of a motion. mVelocityTracker = VelocityTracker.obtain(); } else { // Reset the velocity tracker back to its initial state. mVelocityTracker.clear(); } // Add a user's movement to the tracker. mVelocityTracker.addMovement(event); break; case MotionEvent.ACTION_MOVE: mVelocityTracker.addMovement(event); // When you want to determine the velocity, call // computeCurrentVelocity(). Then call getXVelocity() and // getYVelocity() to retrieve the velocity for each pointer ID. mVelocityTracker.computeCurrentVelocity(1000); // Log velocity of pixels per second. It's best practice to use // VelocityTrackerCompat where possible. Log.d("", "X velocity: " + mVelocityTracker.getXVelocity(pointerId)); Log.d("", "Y velocity: " + mVelocityTracker.getYVelocity(pointerId)); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // Return a VelocityTracker object back to be re-used by others. mVelocityTracker.recycle(); break; } return true; } }
שימוש בלכידת המצביע
באפליקציות מסוימות, כמו משחקים, לקוחות של שולחן עבודה מרוחק ולקוחות של וירטואליזציה, יש יתרון בשליטה על סמן העכבר. תכונת לכידת המצביע זמינה ב-Android מגרסה 8.0 (רמת API 26) ואילך. היא מאפשרת שליטה כזו על ידי העברת כל אירועי העכבר לתצוגה ממוקדת באפליקציה.
בקשה ללכידת מצביע
תצוגה באפליקציה יכולה לבקש ללכוד את מצביע העכבר רק אם היררכיית התצוגות שמכילה אותה נמצאת במוקד. לכן, כדאי לבקש ללכוד את מצביע העכבר כשמתרחשת פעולת משתמש ספציפית בתצוגה, למשל במהלך אירוע onClick() או בגורם המטפל באירועים onWindowFocusChanged() של הפעילות.
כדי לבקש ללכוד את מצביע העכבר, צריך להפעיל את method requestPointerCapture() בתצוגה. בדוגמת הקוד הבאה אפשר לראות איך לבקש ללכוד את מיקום הסמן כשהמשתמש לוחץ על תצוגה:
Kotlin
fun onClick(view: View) { view.requestPointerCapture() }
Java
@Override public void onClick(View view) { view.requestPointerCapture(); }
אחרי שהבקשה ללכידת מצביע מצליחה, מערכת Android קוראת ל-onPointerCaptureChange(true).
המערכת מעבירה את אירועי העכבר לתצוגה שבמוקד באפליקציה, כל עוד היא נמצאת באותה היררכיית תצוגות כמו התצוגה שביקשה את הלכידה. אפליקציות אחרות מפסיקות לקבל אירועי עכבר עד שהלכידה משוחררת, כולל אירועי ACTION_OUTSIDE. מערכת Android מעבירה אירועים של סמן ממקורות אחרים מלבד העכבר כרגיל, אבל סמן העכבר כבר לא גלוי.
טיפול באירועים של מצביעים שנלכדו
אחרי שתצוגה מקבלת בהצלחה את לכידת מצביע העכבר, מערכת Android מעבירה את אירועי העכבר. בתצוגה הממוקדת אפשר לטפל באירועים על ידי ביצוע אחת מהמשימות הבאות:
- אם אתם משתמשים בתצוגה בהתאמה אישית, צריך לבטל את ההגדרה של
onCapturedPointerEvent(MotionEvent). - אחרת, צריך לרשום חשבון
OnCapturedPointerListener.
בדוגמת הקוד הבאה אפשר לראות איך מטמיעים את onCapturedPointerEvent(MotionEvent):
Kotlin
override fun onCapturedPointerEvent(motionEvent: MotionEvent): Boolean { // Get the coordinates required by your app. val verticalOffset: Float = motionEvent.y // Use the coordinates to update your view and return true if the event is // successfully processed. return true }
Java
@Override public boolean onCapturedPointerEvent(MotionEvent motionEvent) { // Get the coordinates required by your app. float verticalOffset = motionEvent.getY(); // Use the coordinates to update your view and return true if the event is // successfully processed. return true; }
בדוגמת הקוד הבאה אפשר לראות איך רושמים OnCapturedPointerListener:
Kotlin
myView.setOnCapturedPointerListener { view, motionEvent -> // Get the coordinates required by your app. val horizontalOffset: Float = motionEvent.x // Use the coordinates to update your view and return true if the event is // successfully processed. true }
Java
myView.setOnCapturedPointerListener(new View.OnCapturedPointerListener() { @Override public boolean onCapturedPointer (View view, MotionEvent motionEvent) { // Get the coordinates required by your app. float horizontalOffset = motionEvent.getX(); // Use the coordinates to update your view and return true if the event is // successfully processed. return true; } });
בין אם משתמשים בתצוגה מותאמת אישית ובין אם רושמים מאזין, התצוגה מקבלת MotionEvent עם קואורדינטות של המצביע שמציינות תנועות יחסיות כמו דלתאות X או Y, בדומה לקואורדינטות שמתקבלות ממכשיר טראקבול. אפשר לאחזר את הקואורדינטות באמצעות getX() ו-getY().
שחרור הלכידה של מצביע העכבר
התצוגה באפליקציה יכולה לבטל את הלכידה של מצביע העכבר באמצעות קריאה ל-releasePointerCapture(), כמו בדוגמה הבאה של קוד:
Kotlin
override fun onClick(view: View) { view.releasePointerCapture() }
Java
@Override public void onClick(View view) { view.releasePointerCapture(); }
המערכת יכולה להסיר את הצילום מהתצוגה בלי שתקראו במפורש ל-releasePointerCapture(), בדרך כלל כי היררכיית התצוגה שמכילה את התצוגה שמבקשת צילום מאבדת את המיקוד.