להפוך תצוגה מותאמת אישית לאינטראקטיבית

אפשר לנסות את הדרך של כתיבת הודעה
‫Jetpack Compose היא ערכת הכלים המומלצת לבניית ממשק משתמש ב-Android. איך עובדים עם פריסות בכתיבת אימייל

ציור ממשק משתמש הוא רק חלק אחד בתהליך של יצירת תצוגה בהתאמה אישית. בנוסף, צריך לוודא שהתצוגה תגיב לקלט של המשתמש באופן שדומה מאוד לפעולה בעולם האמיתי שאתם מחקים.

הקפידו שהאובייקטים באפליקציה יתנהגו כמו אובייקטים אמיתיים. לדוגמה, אל תאפשרו לתמונות באפליקציה להיעלם ולהופיע מחדש במקום אחר, כי אובייקטים בעולם האמיתי לא מתנהגים ככה. במקום זאת, צריך להעביר את התמונות ממקום אחד למקום אחר.

משתמשים חשים אפילו התנהגות או תחושה עדינות בממשק, ומגיבים הכי טוב לניואנסים שמדמים את העולם האמיתי. לדוגמה, כשמשתמשים זורקים אובייקט בממשק המשתמש, כדאי ליצור תחושה של אינרציה בהתחלה כדי לעכב את התנועה. בסוף התנועה, יוצרים תחושה של מומנטום שגורם לאובייקט להמשיך לנוע אחרי ההטלה.

בדף הזה מוסבר איך להשתמש בתכונות של מסגרת Android כדי להוסיף את ההתנהגויות האלה מהעולם האמיתי לתצוגה המותאמת אישית.

מידע נוסף בנושא זמין במאמרים סקירה כללית על אירועי קלט וסקירה כללית על אנימציה של מאפיינים.

ניהול תנועות קלט

בדומה למסגרות רבות אחרות של ממשקי משתמש, מערכת Android תומכת במודל של אירועי קלט. פעולות של משתמשים הופכות לאירועים שמפעילים קריאות חוזרות (callback), ואפשר לבטל את הקריאות החוזרות כדי להתאים אישית את האופן שבו האפליקציה מגיבה למשתמש. אירוע הקלט הנפוץ ביותר במערכת Android הוא מגע, שמפעיל את onTouchEvent(android.view.MotionEvent). כדי לטפל באירוע, מחליפים את השיטה הזו באופן הבא:

Kotlin

override fun onTouchEvent(event: MotionEvent): Boolean {
    return super.onTouchEvent(event)
}

Java

@Override
   public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
   }

אירועי מגע כשלעצמם לא שימושיים במיוחד. ממשקי משתמש מודרניים מבוססי מגע מגדירים אינטראקציות במונחים של מחוות כמו הקשה, משיכה, דחיפה, הטלה ושינוי מרחק התצוגה. כדי להמיר אירועי מגע גולמיים למחוות, מערכת Android מספקת את GestureDetector.

יוצרים GestureDetector על ידי העברת מופע של מחלקה שמטמיעה את GestureDetector.OnGestureListener. אם רוצים לעבד רק כמה תנועות, אפשר להרחיב את המחלקה GestureDetector.SimpleOnGestureListener במקום להטמיע את הממשק GestureDetector.OnGestureListener. לדוגמה, הקוד הזה יוצר מחלקה שמרחיבה את GestureDetector.SimpleOnGestureListener ומבטלת את onDown(MotionEvent).

Kotlin

private val myListener =  object : GestureDetector.SimpleOnGestureListener() {
    override fun onDown(e: MotionEvent): Boolean {
        return true
    }
}

private val detector: GestureDetector = GestureDetector(context, myListener)

Java

class MyListener extends GestureDetector.SimpleOnGestureListener {
   @Override
   public boolean onDown(MotionEvent e) {
       return true;
   }
}
detector = new GestureDetector(getContext(), new MyListener());

גם אם אתם משתמשים ב-GestureDetector.SimpleOnGestureListener, תמיד צריך להטמיע שיטה onDown() שמחזירה true. ההודעה הזו נדרשת כי כל התנועות מתחילות בהודעה onDown(). אם מחזירים את false מ-onDown(), כמו שקורה ב-GestureDetector.SimpleOnGestureListener, המערכת מניחה שרוצים להתעלם משאר תנועת המגע, והשיטות האחרות של GestureDetector.OnGestureListener לא נקראות. מחזירים רק את הערך false מהפונקציה onDown() אם רוצים להתעלם ממחווה שלמה.

אחרי שמטמיעים את GestureDetector.OnGestureListener ויוצרים מופע של GestureDetector, אפשר להשתמש ב-GestureDetector כדי לפרש את אירועי המגע שמתקבלים ב-onTouchEvent().

Kotlin

override fun onTouchEvent(event: MotionEvent): Boolean {
    return detector.onTouchEvent(event).let { result ->
        if (!result) {
            if (event.action == MotionEvent.ACTION_UP) {
                stopScrolling()
                true
            } else false
        } else true
    }
}

Java

@Override
public boolean onTouchEvent(MotionEvent event) {
   boolean result = detector.onTouchEvent(event);
   if (!result) {
       if (event.getAction() == MotionEvent.ACTION_UP) {
           stopScrolling();
           result = true;
       }
   }
   return result;
}

כשמעבירים ל-onTouchEvent() אירוע מגע שהיא לא מזהה כחלק מתנועה, היא מחזירה false. לאחר מכן תוכלו להפעיל קוד משלכם לזיהוי תנועות.

יצירת תנועה שנראית הגיונית מבחינה פיזית

מחוות הן דרך יעילה לשלוט במכשירים עם מסך מגע, אבל הן יכולות להיות לא אינטואיטיביות וקשה לזכור אותן אם הן לא מפיקות תוצאות פיזיות הגיוניות.

לדוגמה, נניח שאתם רוצים להטמיע תנועת החלקה אופקית שגורמת לפריט שמוצג בתצוגה להסתובב סביב הציר האנכי שלו. התנועה הזו הגיונית אם ממשק המשתמש מגיב בתנועה מהירה בכיוון של ההטלה, ואז מאט, כאילו המשתמש דוחף גלגל תנופה וגורם לו להסתובב.

במסמכי התיעוד בנושא הנפשת תנועת גלילה מוסבר בפירוט איך להטמיע התנהגות גלילה משלכם. אבל סימולציה של תחושת גלגל תנופה היא לא פשוטה. כדי שמודל גלגל התנופה יפעל בצורה נכונה, צריך להשתמש בהרבה פיזיקה ומתמטיקה. למזלנו, מערכת Android מספקת מחלקות עזר כדי לדמות את ההתנהגות הזו והתנהגויות אחרות. המחלקה Scroller היא הבסיס לטיפול במחוות של זריקת גלגל תנופה.

כדי להתחיל תנועת החלקה, מפעילים את הפונקציה fling() עם מהירות ההתחלה וערכי המינימום והמקסימום של x ו-y של תנועת ההחלקה. לערך המהירות, אפשר להשתמש בערך שמחושב על ידי GestureDetector.

Kotlin

fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
    scroller.fling(
            currentX,
            currentY,
            (velocityX / SCALE).toInt(),
            (velocityY / SCALE).toInt(),
            minX,
            minY,
            maxX,
            maxY
    )
    postInvalidate()
    return true
}

Java

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   scroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY);
   postInvalidate();
    return true;
}

השיחה אל fling() מגדירה את המודל הפיזיקלי של תנועת ההטלה. לאחר מכן, מעדכנים את Scroller על ידי התקשרות אל Scroller.computeScrollOffset() במרווחי זמן קבועים. ‫computeScrollOffset() מעדכן את המצב הפנימי של אובייקט Scroller על ידי קריאת השעה הנוכחית ושימוש במודל הפיזיקלי כדי לחשב את המיקום x ו-y באותו זמן. מתקשרים אל getCurrX() ואל getCurrY() כדי לאחזר את הערכים האלה.

רוב התצוגות מעבירות את המיקומים x ו-y של אובייקט Scroller ישירות אל scrollTo(). בדוגמה הזו יש הבדל קטן: נעשה בה שימוש במיקום הגלילה הנוכחי x כדי להגדיר את זווית הסיבוב של התצוגה.

Kotlin

scroller.apply {
    if (!isFinished) {
        computeScrollOffset()
        setItemRotation(currX)
    }
}

Java

if (!scroller.isFinished()) {
    scroller.computeScrollOffset();
    setItemRotation(scroller.getCurrX());
}

המחלקות Scroller מחשבות את מיקומי הגלילה בשבילכם, אבל הן לא מחילות את המיקומים האלה על התצוגה באופן אוטומטי. כדאי להחיל קואורדינטות חדשות בתדירות גבוהה מספיק כדי שהאנימציה של הגלילה תיראה חלקה. יש שתי דרכים לעשות את זה:

  • כדי לאלץ ציור מחדש, מפעילים את postInvalidate() אחרי שמפעילים את fling(). כדי להשתמש בטכניקה הזו, צריך לחשב את היסטורי הגלילה ב-onDraw() ולהפעיל את postInvalidate() בכל פעם שהיסטור הגלילה משתנה.
  • מגדירים ValueAnimator כדי להנפיש למשך ההטלה ומוסיפים מאזין לעיבוד עדכוני האנימציה על ידי קריאה ל-addUpdateListener(). הטכניקה הזו מאפשרת להנפיש מאפיינים של View.

איך ליצור מעברים חלקים

המשתמשים מצפים שממשק משתמש מודרני יעבור בצורה חלקה בין מצבים: רכיבי ממשק המשתמש נכנסים ויוצאים בהדרגה במקום להופיע ולנעלם, והתנועות מתחילות ומסתיימות בצורה חלקה במקום להתחיל ולהסתיים בפתאומיות. מסגרת האנימציה של המאפיינים ב-Android מאפשרת ליצור מעברים חלקים בקלות רבה יותר.

כדי להשתמש במערכת האנימציה, בכל פעם שמאפיין משנה את מה שמשפיע על המראה של התצוגה, אל תשנו את המאפיין ישירות. במקום זאת, צריך להשתמש ב-ValueAnimator כדי לבצע את השינוי. בדוגמה הבאה, שינוי רכיב הצאצא שנבחר בתצוגה גורם לסיבוב של כל התצוגה המעובדת כך שהסמן של הבחירה יהיה במרכז. ‫ValueAnimator משנה את הסיבוב במשך כמה מאות אלפיות השנייה, במקום להגדיר באופן מיידי את ערך הסיבוב החדש.

Kotlin

autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0).apply {
    setIntValues(targetAngle)
    duration = AUTOCENTER_ANIM_DURATION
    start()
}

Java

autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0);
autoCenterAnimator.setIntValues(targetAngle);
autoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
autoCenterAnimator.start();

אם הערך שרוצים לשנות הוא אחד מView מאפייני הבסיס, קל עוד יותר ליצור את האנימציה, כי לתצוגות יש ViewPropertyAnimator מובנה שמותאם לאנימציה בו-זמנית של כמה מאפיינים, כמו בדוגמה הבאה:

Kotlin

animate()
    .rotation(targetAngle)
    .duration = ANIM_DURATION
    .start()

Java

animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();