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

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

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

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

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

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

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

טיפול בתנועות קלט

בדומה להרבה מסגרות אחרות של ממשק משתמש, Android תומך במודל של אירועי קלט. משתמשים הפעולות הופכות לאירועים שמפעילים קריאות חוזרות, ואפשר לשנות את קריאה חוזרת (callback) כדי להתאים אישית את האופן שבו האפליקציה מגיבה למשתמש. הקלט הנפוץ ביותר במערכת Android האירוע touch מפעיל, 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 class הוא הבסיס לטיפול בתנועות טיסה בסגנון גלגל תנופה.

כדי להתחיל בקלי קלות, אפשר להתקשר 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 אנימציה של נכס framework מאפשרת מעברים חלקים יותר בקלות.

כדי להשתמש במערכת האנימציה, בכל פעם שנכס משנה את מה שמשפיע המראה של התצוגה המפורטת, אל תשנו את הנכס ישירות. במקום זאת, השתמשו 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();