Android מתקדם ב-Kotlin 03.2: אנימציה עם MotionLayout

1. לפני שמתחילים

ה-Codelab הזה הוא חלק מהקורס Advanced Android ב-Kotlin. כדי שתפיקו את המרב מהקורס הזה אם תעבדו על ה-codelabs ברצף, אבל זו לא חובה. כל שיעורי ה-codelabs מפורטים בדף הנחיתה של Advanced Android ב-Kotlin Codelabs.

MotionLayout היא ספרייה שמאפשרת להוסיף תנועה עשירה לאפליקציה ל-Android. היא מבוססת על ConstraintLayout, ומאפשרת לכם להוסיף אנימציה לכל דבר שאפשר ליצור באמצעות ConstraintLayout.

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

אנימציות הן דרך נהדרת לשפר חוויה באפליקציה. אפשר להשתמש באנימציות כדי:

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

דרישות מוקדמות

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

  • חשוב לדעת איך ליצור אפליקציה עם פעילות ופריסה בסיסית, ולהריץ אותה במכשיר או באמולטור באמצעות Android Studio. חשוב להכיר את ConstraintLayout. כדי לקבל מידע נוסף על ConstraintLayout, אפשר לעיין בקוד Lab לפריסת אילוצים.

מה תעשו

  • הגדרת אנימציה עם ConstraintSets ועם MotionLayout
  • אנימציה על סמך אירועי גרירה
  • שינוי האנימציה עם KeyPosition
  • שינוי המאפיינים באמצעות KeyAttribute
  • הרצת אנימציות עם קוד
  • הוספת אנימציה לכותרות ניתנות לכיווץ בעזרת MotionLayout

מה צריך להכין

  • Android Studio 4.0 (העורך של MotionLayout פועל רק בגרסה הזו של Android Studio.)

2. תחילת העבודה

כדי להוריד את האפליקציה לדוגמה, אפשר:

... או לשכפל את המאגר של GitHub משורת הפקודה באמצעות הפקודה הבאה:

$ git clone https://github.com/googlecodelabs/motionlayout.git

3. יצירת אנימציות באמצעות MotionLayout

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

כדי ליצור אנימציה מקוד הסימן לתחילת פעולה, צריך את החלקים העיקריים הבאים:

  • MotionLayout,, שהיא תת-מחלקה של ConstraintLayout. יש לציין את כל התצוגות שבהן יונפש התג MotionLayout.
  • MotionScene,, שהוא קובץ XML שמתאר אנימציה של MotionLayout.
  • Transition, שהוא חלק מ-MotionScene שמציין את משך האנימציה, את הטריגר ואת אופן העברת התצוגות.
  • ConstraintSet שמציין גם את מגבלות ההתחלה וגם את הסיום של המעבר.

בואו נבחן כל אחד מהם בנפרד, החל ב-MotionLayout.

שלב 1: בודקים את הקוד הקיים

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

  1. באפליקציית res/layout, פותחים את activity_step1.xml. כאן יש ConstraintLayout עם כוכב ImageView אחד שהוחל עליו גוון.

activity_step1.xml

<!-- initial code -->
<androidx.constraintlayout.widget.ConstraintLayout
       ...
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       >

   <ImageView
           android:id="@+id/red_star"
           ...
   />

</androidx.constraintlayout.motion.widget.MotionLayout>

על ConstraintLayout זה אין מגבלות, לכן אם תריצו את האפליקציה עכשיו תראו את תצוגת הכוכב לא מוגבלת, כלומר היא תמוקם במיקום לא ידוע. תוצג ב-Android Studio אזהרה לגבי היעדר מגבלות.

שלב 2: המרה לפריסת תנועה

כדי להוסיף אנימציה באמצעות MotionLayout,, צריך להמיר את ConstraintLayout לMotionLayout.

כדי שהפריסה תתבסס על סצנת תנועה, היא צריכה להצביע אליה.

  1. כדי לעשות את זה, פותחים את פלטפורמת העיצוב. ב-Android Studio 4.0, פותחים את שטח העיצוב באמצעות סמל המפוצל או העיצוב שבפינה השמאלית העליונה, כשמעיינים בקובץ XML לפריסה.

a2beea710c2decb7.png

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

4fa936a98a8393b9.png

הפעולה הזו מחליפה את התג ConstraintLayout בתג MotionLayout ומוסיפה motion:layoutDescription לתג MotionLayout שמצביע אל @xml/activity_step1_scene.

activity_step1**.xml**

<!-- explore motion:layoutDescription="@xml/activity_step1_scene" -->
<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       motion:layoutDescription="@xml/activity_step1_scene">

סצנה של תנועה היא קובץ XML יחיד שמתאר אנימציה ב-MotionLayout.

מיד אחרי ההמרה ל-MotionLayout, במסך העיצוב יוצג ה'עורך השינויים'

66d0e80d5ab4daf8.png

בעורך התנועה יש שלושה רכיבים חדשים בממשק המשתמש:

  1. סקירה כללית – זוהי אפשרות בחירה מודאלית שמאפשרת לבחור חלקים שונים של האנימציה. בתמונה הזו נבחר המאפיין start ConstraintSet. אפשר גם לבחור את המעבר בין start לבין end על-ידי לחיצה על החץ ביניהם.
  2. קטע – מתחת לסקירה הכללית מופיע חלון של קטע שמשתנה בהתאם לפריט הסקירה הכללית הנוכחי שנבחר. בתמונה הזו, המידע start ConstraintSet מוצג בחלון הבחירה.
  3. מאפיין – חלונית המאפיינים מוצגת ומאפשרת לערוך את המאפיינים של הפריט הנוכחי שנבחר דרך חלון הסקירה הכללית או חלון הבחירה. בתמונה הזו מוצגים המאפיינים של start ConstraintSet.

שלב 3: הגדרת מגבלות התחלה וסיום

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

הפרמטר MotionScene משתמש בתג ConstraintSet כדי להגדיר את מצבי ההתחלה והסיום. ConstraintSet הוא האופן שבו הוא נשמע, קבוצת אילוצים שאפשר להחיל על צפיות. כולל מגבלות של רוחב, גובה ו-ConstraintLayout. הוא כולל גם כמה מאפיינים כמו alpha. הוא לא מכיל את התצוגות עצמן, אלא רק את המגבלות על הצפיות האלה.

כל אילוצים שצוינו ב-ConstraintSet יבטלו את המגבלות שצוינו בקובץ הפריסה. אם מגדירים אילוצים גם בפריסה וגם ב-MotionScene, יחולו רק האילוצים ב-MotionScene.

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

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

  1. צריך לבחור את ConstraintSet start בחלונית הסקירה הכללית

6e57661ed358b860.png

  1. בחלונית הבחירה בוחרים באפשרות red_star. כרגע מוצג בו המקור של layout, כלומר הוא לא מוגבל בConstraintSet. לוחצים על סמל העיפרון בפינה השמאלית העליונה כדי ליצור הגבלה.

f9564c574b86ea8.gif

  1. מוודאים ש-red_star מציג מקור של start כאשר start ConstraintSet נבחר בחלונית הסקירה הכללית.
  2. בחלונית 'מאפיינים', שבה האפשרות red_star נבחרה בstart ConstraintSet, מוסיפים הגבלה בחלק העליון ומתחילים ללחוץ על לחצני + הכחולים.

2fce076cd7b04bd.png

  1. פותחים את xml/activity_step1_scene.xml כדי לראות את הקוד שנוצר על ידי Motion Editor לאילוץ הזה.

activity_step1_scene.xml

<!-- Constraints to apply at the start of the animation -->
<ConstraintSet android:id="@+id/start">
   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

ב-ConstraintSet יש id של @id/start, והוא מציין את כל המגבלות שצריך להחיל על כל התצוגות המפורטות ב-MotionLayout. בMotionLayout יש רק תצוגה אחת, לכן צריך רק Constraint אחת.

השדה Constraint שבתוך ConstraintSet מציין את מזהה התצוגה המפורטת שהוא מגביל, @id/red_star שמוגדר ב-activity_step1.xml. חשוב לציין שתגי Constraint מציינים רק אילוצים ופרטי פריסה. לתג Constraint לא ידוע שהוא מוחל על ImageView.

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

  1. צריך לבחור באפשרות end ConstraintSet בחלונית הסקירה הכללית.

346e1248639b6f1e.png

  1. עליך לפעול לפי אותם השלבים שביצעת לפני כן כדי להוסיף Constraint לred_star בend ConstraintSet.
  2. כדי להשתמש ב'עורך התנועה' להשלמת השלב הזה, מוסיפים אילוץ ל-bottom ול-end על ידי לחיצה על לחצני + הכחולים.

fd33c779ff83c80a.png

  1. הקוד ב-XML נראה כך:

activitiy_step1_scene.xml

<!-- Constraints to apply at the end of the animation -->
<ConstraintSet android:id="@+id/end">
   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>

בדיוק כמו @id/start, גם לConstraintSet הזה יש Constraint יחיד ב-@id/red_star. הפעם היא מגבילה אותו לקצה התחתון של המסך.

אינך חייב לתת להן שם @id/start ו-@id/end, אך נוח לעשות זאת.

שלב 4: הגדרת מעבר

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

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

  1. Motion Editor יצר עבורנו מעבר כברירת מחדל כאשר הוא יצר את הקובץ MotionScene. כדי לראות את המעבר שנוצר, צריך לפתוח את activity_step1_scene.xml.

activity_step1_scene.xml

<!-- A transition describes an animation via start and end state -->
<Transition
   motion:constraintSetEnd="@+id/end"
   motion:constraintSetStart="@id/start"
   motion:duration="1000">
  <KeyFrameSet>
  </KeyFrameSet>
</Transition>

זה כל מה שצריך ל-MotionLayout כדי ליצור אנימציה. בודקים כל מאפיין:

  • constraintSetStart יוחל על הצפיות עם תחילת האנימציה.
  • constraintSetEnd יוחל על הצפיות בסוף האנימציה.
  • duration מציין כמה זמן האנימציה צריכה להימשך באלפיות השנייה.

לאחר מכן, MotionLayout יקבע את הנתיב בין אילוצי ההתחלה והסיום ותוסיף לו אנימציה למשך הזמן שצוין.

שלב 5: תצוגה מקדימה של האנימציה ב-Motion Editor

dff9ecdc1f4a0740.gif

אנימציה: סרטון של הפעלת תצוגה מקדימה של מעבר בעורך התנועה

  1. פותחים את 'עורך התנועה' ובוחרים את המעבר באמצעות לחיצה על החץ בין start לבין end בחלונית הסקירה הכללית.

1dc541ae8c43b250.png

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

a0fd2593384dfb36.png

שלב 6: הוספה של handler של קליקים

צריכה להיות לכם דרך להתחיל את האנימציה. דרך אחת לעשות זאת היא לגרום ל-MotionLayout להגיב לאירועי קליקים ב-@id/red_star.

  1. פותחים את עורך התנועה ובוחרים במעבר על ידי לחיצה על החץ בין ההתחלה לסיום בחלונית הסקירה הכללית.

b6f94b344ce65290.png

  1. לוחצים על 699f7ae04024ccf6.png יצירת handler של לחיצה או החלקה בסרגל הכלים של חלונית הסקירה הכללית . הפעולה הזו מוסיפה handler שיתחיל מעבר.
  2. בוחרים באפשרות מטפל קליקים בחלון הקופץ

ccf92d06335105fe.png

  1. משנים את הערך View to click ל-red_star.

b0d3f0c970604f01.png

  1. לוחצים על Add (הוספה). מטפל הקליקים מיוצג על ידי נקודה קטנה בכלי המעבר בעורך התנועה.

cec3913e67fb4105.png

  1. כשהמעבר נבחר בחלונית הסקירה הכללית, מוסיפים מאפיין clickAction של toggle ל-handler של OnClick שהוספתם עכשיו בחלונית המאפיינים.

9af6fc60673d093d.png

  1. אפשר לפתוח את activity_step1_scene.xml כדי לראות את הקוד שנוצר על ידי Motion Editor

activity_step1_scene.xml

<!-- A transition describes an animation via start and end state -->
<Transition
    motion:constraintSetStart="@+id/start"
    motion:constraintSetEnd="@+id/end"
    motion:duration="1000">
    <!-- MotionLayout will handle clicks on @id/red_star to "toggle" the animation between the start and end -->
    <OnClick
        motion:targetId="@id/red_star"
        motion:clickAction="toggle" />
</Transition>

התג Transition מורה ל-MotionLayout להריץ את האנימציה בתגובה לאירועי קליק, באמצעות תג <OnClick>. בודקים כל מאפיין:

  • targetId הוא התצוגה למעקב אחר קליקים.
  • clickAction מתוך toggle יעבור בין מצב ההתחלה למצב הסיום בלחיצה. אפשר לקרוא על אפשרויות נוספות לגבי clickAction במסמכי התיעוד.
  1. מריצים את הקוד, לוחצים על שלב 1, ולאחר מכן לוחצים על הכוכב האדום ורואים את האנימציה!

שלב 5: אנימציות בפעולה

הפעלת האפליקציה! האנימציה אמורה לפעול כשלוחצים על הכוכב.

7ba88af963fdfe10.gif

בקובץ תמונת התנועה שהושלם מוגדר Transition אחד שמצביע להתחלה ולסיום ConstraintSet.

בתחילת האנימציה (@id/start), סמל הכוכב מוגבל לחלק העליון של המסך. בסוף האנימציה (@id/end), סמל הכוכב מוגבל בקצה התחתון של המסך.

<?xml version="1.0" encoding="utf-8"?>

<!-- Describe the animation for activity_step1.xml -->
<MotionScene xmlns:app="http://schemas.android.com/apk/res-auto"
            xmlns:android="http://schemas.android.com/apk/res/android">
   <!-- A transition describes an animation via start and end state -->
   <Transition
           motion:constraintSetStart="@+id/start"
           motion:constraintSetEnd="@+id/end"
           motion:duration="1000">
       <!-- MotionLayout will handle clicks on @id/star to "toggle" the animation between the start and end -->
       <OnClick
               motion:targetId="@id/red_star"
               motion:clickAction="toggle" />
   </Transition>

   <!-- Constraints to apply at the end of the animation -->
   <ConstraintSet android:id="@+id/start">
       <Constraint
               android:id="@+id/red_star"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               motion:layout_constraintStart_toStartOf="parent"
               motion:layout_constraintTop_toTopOf="parent" />
   </ConstraintSet>

   <!-- Constraints to apply at the end of the animation -->
   <ConstraintSet android:id="@+id/end">
       <Constraint
               android:id="@+id/red_star"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               motion:layout_constraintEnd_toEndOf="parent"
               motion:layout_constraintBottom_toBottomOf="parent" />
   </ConstraintSet>
</MotionScene>

4. אנימציה על סמך אירועי גרירה

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

שלב 1: בודקים את הקוד הראשוני

  1. כדי להתחיל, צריך לפתוח את קובץ הפריסה activity_step2.xml, שיש בו MotionLayout קיים. כדאי להציץ בקוד.

activity_step2.xml

<!-- initial code -->

<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       motion:layoutDescription="@xml/step2" >

   <ImageView
           android:id="@+id/left_star"
           ...
   />

   <ImageView
           android:id="@+id/right_star"
           ...
   />

   <ImageView
           android:id="@+id/red_star"
           ...
   />

   <TextView
           android:id="@+id/credits"
           ...
           motion:layout_constraintTop_toTopOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.motion.widget.MotionLayout>

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

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

שלב 2: מוסיפים אנימציה לסצנה

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

מגדירים את ערך ההתחלה של ConstraintSet.

  1. פותחים את סצנת התנועה xml/step2.xml כדי להגדיר את האנימציה.
  2. מוסיפים את המגבלות של האילוץ ההתחלתי start. בהתחלה, כל שלושת הכוכבים מרוכזים בחלק התחתון של המסך. לכוכבים הימניים והשמאליים יש ערך alpha של 0.0, כלומר הם שקופים ומוסתרים לחלוטין.

step2.xml

<!-- TODO apply starting constraints -->

<!-- Constraints to apply at the start of the animation -->
<ConstraintSet android:id="@+id/start">
   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />

   <Constraint
           android:id="@+id/left_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="0.0"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />

   <Constraint
           android:id="@+id/right_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="0.0"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>

בפונקציה ConstraintSet, מציינים Constraint אחד לכל אחד מהכוכבים. כל אילוץ יוחל על ידי MotionLayout בתחילת האנימציה.

כל תצוגת כוכב ממורכזת בחלק התחתון של המסך לפי מגבלות התחלה, סיום ותחתון. לשני הכוכבים @id/left_star ו-@id/right_star יש ערך אלפא נוסף שהופך אותם לבלתי נראים, והמערכת תחיל אותם בתחילת האנימציה.

קבוצות האילוצים start ו-end מגדירות את ההתחלה והסיום של האנימציה. מגבלה בהתחלה, למשל motion:layout_constraintStart_toStartOf, תגביל את ההתחלה של תצוגה מפורטת אחרת. זה עלול לבלבל בהתחלה, כי השם start משמש גם וגם בהקשר של אילוצים. כדי לעזור לך לבדל את ההבחנה, start בlayout_constraintStart מתייחס ל'התחלה' של התצוגה, שנמצאת משמאל לימין בשפה משמאל לימין וימין בשפה מימין לשמאל. קבוצת האילוצים start מתייחסת להתחלת האנימציה.

מגדירים את ערך ה-ConstraintSet הסופי

  1. מגדירים את אילוץ הסיום לשימוש בשרשרת כדי למקם את כל שלושת הכוכבים יחד מתחת ל-@id/credits. כמו כן, היא תגדיר את ערך הסיום של alpha של הכוכבים הימניים והשמאליים כ-1.0.

step2.xml

<!-- TODO apply ending constraints -->

<!-- Constraints to apply at the end of the animation -->
<ConstraintSet android:id="@+id/end">

   <Constraint
           android:id="@+id/left_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="1.0"
           motion:layout_constraintHorizontal_chainStyle="packed"
           motion:layout_constraintStart_toStartOf="parent"
           motion:layout_constraintEnd_toStartOf="@id/red_star"
           motion:layout_constraintTop_toBottomOf="@id/credits" />

   <Constraint
           android:id="@+id/red_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           motion:layout_constraintStart_toEndOf="@id/left_star"
           motion:layout_constraintEnd_toStartOf="@id/right_star"
           motion:layout_constraintTop_toBottomOf="@id/credits" />

   <Constraint
           android:id="@+id/right_star"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:alpha="1.0"
           motion:layout_constraintStart_toEndOf="@id/red_star"
           motion:layout_constraintEnd_toEndOf="parent"
           motion:layout_constraintTop_toBottomOf="@id/credits" />
</ConstraintSet>

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

בנוסף, מכיוון שהמאפיין alpha מוגדר ב-@id/right_start וב-@id/left_star ב-ConstraintSets, שתי התצוגות יהפוך לשקוף בהדרגה עם התקדמות האנימציה.

אנימציה על סמך החלקה של המשתמש

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

  1. מחליפים את הפעולה לביצוע להוספת תג OnSwipe ב-<OnSwipe motion:touchAnchorId="@id/red_star" />.

step2.xml

<!-- TODO add OnSwipe tag -->

<!-- A transition describes an animation via start and end state -->
<Transition
       motion:constraintSetStart="@+id/start"
       motion:constraintSetEnd="@+id/end">
   <!-- MotionLayout will track swipes relative to this view -->
   <OnSwipe motion:touchAnchorId="@id/red_star" />
</Transition>

OnSwipe מכיל כמה מאפיינים, והחשוב ביותר הוא touchAnchorId.

  • touchAnchorId היא התצוגה שבמעקב, שזזה בתגובה למגע. המרחק מהתצוגה הזו על ידי MotionLayout יישאר זהה מהאצבע שמחליקה.
  • touchAnchorSide קובע אחרי איזה צד של התצוגה צריך לעקוב. הדבר חשוב בתצוגות שגודלן משתנה, שהאורך שלהן מורכב מנתיבים מורכבים או כשיש צד אחד שזז מהר יותר מהצד השני.
  • dragDirection קובע איזה כיוון חשוב לאנימציה הזו (למעלה, למטה, שמאלה או ימינה).

כש-MotionLayout מקשיב לאירועי גרירה, ה-listener נרשם בתצוגה של MotionLayout ולא בתצוגה שצוינה על ידי touchAnchorId. כשמשתמש מתחיל תנועה במקום כלשהו במסך, MotionLayout שומר על המרחק בין האצבע לבין touchAnchorSide הקבוע של תצוגת touchAnchorId. לדוגמה, אם הם נוגעים במרחק 100dp מהצד של העוגן, MotionLayout ירחיק את הצד הזה מהאצבע במשך כל האנימציה.

רוצה לנסות?

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

fefcdd690a0dcaec.gif

MotionLayout יכול ליצור אנימציה בין עיצובים שונים באמצעות התכונות של ConstraintLayout, ליצירת אפקטים עשירים.

באנימציה הזו, כל שלוש התצוגות ממוקמות ביחס להורה שלהן בתחתית המסך כדי להתחיל. בסוף, שלוש התצוגות ממוקמות ביחס ל-@id/credits ברשת.

למרות הפריסות השונות האלה, MotionLayout ייצור אנימציה חלקה בין ההתחלה לסיום.

5. שינוי נתיב

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

שלב 1: בודקים את הקוד הקיים

  1. אפשר לפתוח את layout/activity_step3.xml ואת xml/step3.xml כדי לראות את הפריסה הקיימת וסצנה של התנועה. ImageView ו-TextView מציגים את טקסט הירח והקרדיטים.
  2. פותחים את הקובץ של סצנת התנועה (xml/step3.xml). אתם יכולים לראות שמוגדר Transition מ-@id/start עד @id/end. האנימציה מעבירה את תמונת הירח מהפינה השמאלית התחתונה של המסך לפינה הימנית התחתונה של המסך באמצעות שני ConstraintSets. טקסט הקרדיטים הופך לשקוף בהדרגה מ-alpha="0.0" ל-alpha="1.0" בזמן שהירח זז.
  3. מפעילים את האפליקציה עכשיו ובוחרים באפשרות שלב 3. כשתלחצו על הירח, תראו שהירח עובר דרך נתיב ליניארי (או קו ישר) מההתחלה ועד הסוף.

שלב 2: מפעילים ניפוי באגים בנתיב

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

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

  1. כדי להפעיל נתיבים לניפוי באגים, פותחים את layout/activity_step3.xml ומוסיפים את הפרמטר motion:motionDebug="SHOW_PATH" לתג MotionLayout.

activity_step3.xml

<!-- Add motion:motionDebug="SHOW_PATH" -->

<androidx.constraintlayout.motion.widget.MotionLayout
       ...
       motion:motionDebug="SHOW_PATH" >

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

23bbb604f456f65c.png

  • מעגלים מייצגים את מיקום ההתחלה או הסיום של תצוגה אחת.
  • קווים מייצגים את הנתיב של תצוגה אחת.
  • יהלומים מייצגים KeyPosition שמשנה את הנתיב.

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

שלב 3: שינוי נתיב

כל האנימציות ב-MotionLayout מוגדרות לפי התחלה וסיום ב-ConstraintSet, שמגדירות איך המסך ייראה לפני תחילת האנימציה ואחריה. כברירת מחדל, MotionLayout משרטט נתיב ליניארי (קו ישר) בין נקודת ההתחלה והסיום של כל תצוגה שמשנה את המיקום.

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

  1. צריך לפתוח את xml/step3.xml ולהוסיף KeyPosition לסצנה. התג KeyPosition ממוקם בתוך התג Transition.

eae4dae9a12d0410.png

step3.xml

<!-- TODO: Add KeyFrameSet and KeyPosition -->
<KeyFrameSet>
   <KeyPosition
           motion:framePosition="50"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.5"
   />
</KeyFrameSet>

KeyFrameSet הוא צאצא של Transition, והוא קבוצה של כל הKeyFrames, כמו KeyPosition, שצריך להחיל במהלך המעבר.

מכיוון ש-MotionLayout מחשבת את הנתיב של הירח בין ההתחלה לסוף, היא משנה את הנתיב על סמך ה-KeyPosition שמצוין ב-KeyFrameSet. תוכלו להריץ שוב את האפליקציה כדי לראות איך הפעולה הזו משנה את הנתיב.

ל-KeyPosition יש כמה מאפיינים שמתארים איך הוא משנה את הנתיב. הגורמים החשובים ביותר הם:

  • framePosition הוא מספר בין 0 ל-100. הוא מגדיר מתי באנימציה יש להחיל את KeyPosition: 1% הוא 1% לאורך האנימציה ו-99 מייצג 99% דרך האנימציה. אם הערך הוא 50, מחילים אותו בדיוק באמצע.
  • motionTarget היא התצוגה המפורטת שעבורה KeyPosition משנה את הנתיב.
  • keyPositionType הוא האופן שבו KeyPosition משנה את הנתיב. הוא יכול להיות parentRelative, pathRelative או deltaRelative (כפי שמוסבר בשלב הבא).
  • percentX | percentY הוא המספר שיש לשנות את הנתיב ב-framePosition (מותר להזין ערכים בין 0.0 ל-1.0, עם ערכים שליליים וערכים שליליים גדולים מ-1).

אפשר לחשוב על זה כך: "ב-framePosition משנים את הנתיב של motionTarget על ידי הזזת הנתיב ב-percentX או percentY בהתאם לקואורדינטות שנקבעות על ידי keyPositionType."

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

רוצים לנסות?

אם תפעילו שוב את האפליקציה, תוצג האנימציה של השלב הזה.

46b179c01801f19e.gif

הירח מופיע אחרי קשת כי הוא עובר דרך KeyPosition שמצוין בTransition.

<KeyPosition
       motion:framePosition="50"
       motion:motionTarget="@id/moon"
       motion:keyPositionType="parentRelative"
       motion:percentY="0.5"
/>

אפשר לקרוא את הKeyPosition הזה כך: "ב-framePosition 50 (באמצע האנימציה) משנה את הנתיב של motionTarget @id/moon על-ידי הזזת הנתיב ב-50% Y (חצי למטה במסך) בהתאם לקואורדינטות שנקבעות על ידי parentRelative (MotionLayout כולו)."

לכן, באמצע האנימציה, הירח חייב לעבור דרך KeyPosition שנמצאת ב-50% מהמסך. KeyPosition לא משנה בכלל את התנועה ב-X, כך שהירח עדיין יתקדם מתחילתו ועד סופו לרוחב. MotionLayout יקבע נתיב חלק שעובר דרך KeyPosition בזמן שעובר בין ההתחלה לסוף.

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

1c7cf779931e45cc.gif

<Constraint
       android:id="@id/credits"
       ...
       motion:layout_constraintBottom_toBottomOf="@id/moon"
       motion:layout_constraintTop_toTopOf="@id/moon"
/>

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

אם רצית שהקרדיטים יועברו לירח, אפשר להוסיף KeyPosition לקרדיטים או לשנות את מגבלות ההתחלה ב-@id/credits.

בקטע הבא מתעמקים בסוגים השונים של keyPositionType בMotionLayout.

6. הסבר על keyPositionType

בשלב האחרון השתמשת בסוג keyPosition של parentRelative כדי לקזז את הנתיב ב-50% מהמסך. המאפיין keyPositionType קובע איך MotionLayout ישנה את הנתיב בהתאם ל-percentX או ל-percentY.

<KeyFrameSet>
   <KeyPosition
           motion:framePosition="50"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.5"
   />
</KeyFrameSet>

יש 3 סוגים של keyPosition סוגים שונים: parentRelative, pathRelative וdeltaRelative. ציון הסוג יגרום לשינוי של מערכת הקואורדינטות שבה המערכת מחשבת את percentX ו-percentY.

מהי מערכת קואורדינטות?

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

מערכות הקואורדינטות MotionLayout הן מערכת קואורדינטות קרטזית. כלומר, יש להם ציר X ו-Y שמוגדרים על ידי שני קווים מאונך. ההבדל העיקרי ביניהם הוא המיקום במסך שבו נמצא ציר ה-X (ציר ה-Y תמיד מאונך לציר ה-X).

כל מערכות הקואורדינטות ב-MotionLayout משתמשות בערכים בין 0.0 ל-1.0 גם בציר ה-X וגם בציר ה-Y. הם מאפשרים ערכים שליליים וערכים שגדולים מ-1.0. כך, לדוגמה, אם ערך percentX הוא -2.0, צריך להופיע פעמיים בכיוון הנגדי של ציר ה-X.

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

קואורדינטות יחסיות של הורה

a7b7568d46d9dec7.png

ב-keyPositionType של parentRelative נעשה שימוש באותה מערכת קואורדינטות כמו במסך. הוא מגדיר את (0, 0) מצד שמאל למעלה של MotionLayout כולו, ואת (1, 1) מצד ימין למטה.

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

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

קואורדינטות יחסיות

5680bf553627416c.png

דלתא היא מונח מתמטי לשינוי, כך שdeltaRelative הוא דרך לומר 'שינוי יחסי'. בקואורדינטות deltaRelative(0,0) הוא המיקום ההתחלתי של התצוגה, ו-(1,1) הוא המיקום הסופי. הצירים X ו-Y מיושרים למסך.

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

deltaRelative הוא מערכת קואורדינטות מעולה לשליטה בתנועה האופקית או האנכית באופן מבודד. לדוגמה, אפשר ליצור אנימציה שמשלימה רק את התנועה האנכית (Y) שלה ב-50%, וממשיכה להוסיף אנימציה לרוחב (X).

קואורדינטות יחסיות

f3aaadaac8b4a93f.png

מערכת הקואורדינטות האחרונה ב-MotionLayout היא pathRelative. הוא שונה מהשניים האחרים, מכיוון שציר ה-X עוקב אחר נתיב התנועה מתחילתו ועד סופו. לכן, (0,0) הוא מיקום ההתחלה, ו-(1,0) הוא מיקום הסיום.

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

מתברר ש-pathRelative ממש שימושי לכמה דברים.

  • האצה, האטה או עצירה של צפייה במהלך חלק מהאנימציה. המימד X תמיד יתאים בדיוק לנתיב בתצוגה המפורטת, ולכן אפשר להשתמש ב-pathRelative KeyPosition כדי לשנות את framePosition הנקודה המסוימת בנתיב הזה. לכן, KeyPosition ב-framePosition="50" עם percentX="0.1" יגרום לאנימציה להימשך 50% מהזמן ב-10% הראשונים של התנועה.
  • הוספת קשת מעודנת לנתיב. מכיוון שהממד Y תמיד מאונך לתנועה, שינוי של Y ישנה את הנתיב לעקומה ביחס לתנועה הכוללת.
  • הוספת מאפיין שני כאשר deltaRelative לא תפעל. לתנועה אופקית ואנכית לחלוטין, deltaRelative ייצור רק מימד שימושי אחד. עם זאת, pathRelative תמיד ייצור קואורדינטות X ו-Y שניתן להשתמש בהן.

בשלב הבא תלמדו איך ליצור נתיבים מורכבים יותר באמצעות יותר משדה KeyPosition אחד.

7. מסלולים מורכבים של מבנים

בחינת האנימציה שבנית בשלב האחרון יוצרת עקומה חלקה, אבל הצורה יכולה להיות יותר כמו על ירח.

שינוי נתיב שיש בו כמה רכיבי KeyPosition

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

  1. פתיחת xml/step4.xml. אפשר לראות שיש לו את אותן הצפיות וגם את KeyFrame שהוספת בשלב האחרון.
  2. כדי לעגל את החלק העליון של העקומה, יש להוסיף עוד שני ערכי KeyPositions לנתיב של @id/moon, אחד ממש לפני שהוא מגיע לראש העקומה ואחת אחריו.

500b5ac2db48ef87.png

step4.xml

<!-- TODO: Add two more KeyPositions to the KeyFrameSet here -->
<KeyPosition
       motion:framePosition="25"
       motion:motionTarget="@id/moon"
       motion:keyPositionType="parentRelative"
       motion:percentY="0.6"
/>
<KeyPosition
       motion:framePosition="75"
       motion:motionTarget="@id/moon"
       motion:keyPositionType="parentRelative"
       motion:percentY="0.6"
/>

KeyPositions יחולו 25% ו-75% מאורך האנימציה, ויגרמו ל-@id/moon לעבור בנתיב של 60% מהחלק העליון של המסך. בשילוב עם KeyPosition הקיים ב-50%, נוצרת קשת חלקה שעקבו אחרי הירח.

ב-MotionLayout, אפשר להוסיף כמה KeyPositions שצריך כדי לקבל את נתיב התנועה הרצוי. MotionLayout יחיל כל KeyPosition בframePosition שצוין, ו יבין איך ליצור תנועה חלקה שעוברת בכל הKeyPositions.

רוצים לנסות?

  1. מפעילים שוב את האפליקציה. עוברים אל שלב 4 כדי לראות את האנימציה בפעולה. כשלוחצים על הירח, המסלול עובר מתחילתו ועד סופו דרך כל KeyPosition שצוין בKeyFrameSet.

רוצה לחקור לבד?

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

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

cd9faaffde3dfef.png

step4.xml

<!-- Complex paths example: Dancing moon -->
<KeyFrameSet>
   <KeyPosition
           motion:framePosition="25"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.6"
           motion:percentX="0.1"
   />
   <KeyPosition
           motion:framePosition="50"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.5"
           motion:percentX="0.3"
   />
   <KeyPosition
           motion:framePosition="75"
           motion:motionTarget="@id/moon"
           motion:keyPositionType="parentRelative"
           motion:percentY="0.6"
           motion:percentX="0.1"
   />
</KeyFrameSet>

אחרי שמסיימים לחקור את KeyPosition, בשלב הבא עוברים לסוגים אחרים של KeyFrames.

8. שינוי מאפיינים במהלך תנועה

כדי ליצור אנימציות דינמיות בדרך כלל צריך לשנות את הצפיות size, rotation או alpha ככל שהאנימציה מתקדם. MotionLayout תומך באנימציה של מאפיינים רבים בכל תצוגה באמצעות KeyAttribute.

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

שלב 1: שינוי הגודל וסיבוב באמצעות KeyAttribute

  1. פותחים את xml/step5.xml, שמכילה את אותה האנימציה שיצרתם בשלב האחרון. כדי גיוון, המסך הזה משתמש בתמונת מרחב שונה כרקע.
  2. כדי להגדיל את גודל הירח ולהסתובב, צריך להוסיף שני תגי KeyAttribute בKeyFrameSet בkeyFrame="50" ובkeyFrame="100"

bbae524a2898569.png

step5.xml

<!-- TODO: Add KeyAttributes to rotate and resize @id/moon -->

<KeyAttribute
       motion:framePosition="50"
       motion:motionTarget="@id/moon"
       android:scaleY="2.0"
       android:scaleX="2.0"
       android:rotation="-360"
/>
<KeyAttribute
       motion:framePosition="100"
       motion:motionTarget="@id/moon"
       android:rotation="-720"
/>

הKeyAttributes האלה מוחלים ב-50% ו-100% מהאנימציה. KeyAttribute הראשון בציון 50% יתבצע בחלק העליון של הקשת, ויגרום להכפלת הגודל של התצוגה ולסיבוב של -360 מעלות (או מעגל מלא אחד). הסיבוב השני של הKeyAttribute יסיים את הסיבוב השני ל-720 מעלות (שני עיגולים שלמים) ויכווץ את הגודל בחזרה לרגיל, כי הערכים של scaleX ו-scaleY הם ערכי ברירת מחדל של 1.0.

בדיוק כמו KeyPosition, גם KeyAttribute משתמש ב-framePosition וב-motionTarget כדי לציין מתי להחיל את KeyFrame ואיזו תצוגה לשנות. MotionLayout יבצע אינטרפולציה בין KeyPositions כדי ליצור אנימציות נוזליות.

יש תמיכה במאפיינים של KeyAttributes שאפשר להחיל על כל התצוגות. הם תומכים בשינוי של מאפיינים בסיסיים כמו visibility, alpha או elevation. אפשר גם לשנות את הסיבוב כמו שעושים כאן, לסובב את התצוגה בשלוש מידות באמצעות rotateX ו-rotateY, לשנות את הגודל באמצעות scaleX ו-scaleY או לתרגם את מיקום התצוגה ב-X, ב-Y או ב-Z.

שלב 2: דחיית ההצגה של זיכויים

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

  1. כדי לעכב את ההופעה של זיכויים, צריך להגדיר עוד KeyAttribute אחד שמבטיח ש-alpha יישאר 0 עד keyPosition="85". MotionLayout עדיין יעבור בצורה חלקה מ-0 ל-100 אלפא, אבל הוא יתבצע במהלך 15% האחרונים של האנימציה.

step5.xml

<!-- TODO: Add KeyAttribute to delay the appearance of @id/credits -->

<KeyAttribute
       motion:framePosition="85"
       motion:motionTarget="@id/credits"
       android:alpha="0.0"
/>

כך KeyAttribute שומר את alpha מתוך @id/credits ב-0.0 עבור 85% הראשונים של האנימציה. מאחר שהמחרוזת מתחילה באלפא של 0, היא לא תהיה גלויה ב-85% הראשונים של האנימציה.

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

על ידי השהיית אנימציות בתצוגה אחת בזמן שתצוגה אחרת זזה, ניתן ליצור אנימציות מרשימות עם תחושה דינמית של המשתמש.

רוצים לנסות?

  1. מפעילים שוב את האפליקציה ועוברים לשלב 5 כדי לראות את האנימציה בפעולה. כשלוחצים על הירח, הוא יעקוב אחרי הנתיב מתחילתו ועד סופו, דרך כל KeyAttribute שצוין בKeyFrameSet.

2f4bfdd681c1fa98.gif

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

גילוי עצמאי

לפני שממשיכים לסוג הסופי של KeyFrame, כדאי לנסות לשנות מאפיינים רגילים אחרים בKeyAttributes. לדוגמה, אפשר לנסות לשנות את rotation ל-rotationX כדי לראות איזו אנימציה היא יוצרת.

רשימה של המאפיינים הרגילים שאפשר לנסות:

  • android:visibility
  • android:alpha
  • android:elevation
  • android:rotation
  • android:rotationX
  • android:rotationY
  • android:scaleX
  • android:scaleY
  • android:translationX
  • android:translationY
  • android:translationZ

9. שינוי מאפיינים מותאמים אישית

אנימציות עשירות כוללות שינוי הצבע או מאפיינים אחרים של תצוגה. ל-MotionLayout יש אפשרות להשתמש ב-KeyAttribute כדי לשנות כל אחד מהמאפיינים הרגילים שמפורטים במשימה הקודמת, אבל המאפיין CustomAttribute משמש לציון כל מאפיין אחר.

אפשר להשתמש ב-CustomAttribute כדי להגדיר כל ערך שיש לו מגדיר. לדוגמה, אפשר להגדיר את backgroundColor בתצוגה באמצעות CustomAttribute. MotionLayout ישתמש בהשתקפות כדי למצוא את הרכיב המגדיר ואז ישתמש בו שוב ושוב כדי להוסיף אנימציה לתצוגה.

בשלב הזה צריך להשתמש ב-CustomAttribute כדי להגדיר את המאפיין colorFilter בירח כדי לבנות את האנימציה שמוצגת למטה.

5fb6792126a09fda.gif

הגדרה של מאפיינים מותאמים אישית

  1. כדי להתחיל, פותחים את xml/step6.xml, שמכיל את אותה האנימציה שיצרתם בשלב האחרון.
  2. כדי לשנות את צבע הירח, מוסיפים שני KeyAttribute עם CustomAttribute בKeyFrameSet בשעה keyFrame="0", keyFrame="50" ו-keyFrame="100".

214699d5fdd956da.png

step6.xml

<!-- TODO: Add Custom attributes here -->
<KeyAttribute
       motion:framePosition="0"
       motion:motionTarget="@id/moon">
   <CustomAttribute
           motion:attributeName="colorFilter"
           motion:customColorValue="#FFFFFF"
   />
</KeyAttribute>
<KeyAttribute
       motion:framePosition="50"
       motion:motionTarget="@id/moon">
   <CustomAttribute
           motion:attributeName="colorFilter"
           motion:customColorValue="#FFB612"
   />
</KeyAttribute>
<KeyAttribute
       motion:framePosition="100"
       motion:motionTarget="@id/moon">
   <CustomAttribute
           motion:attributeName="colorFilter"
           motion:customColorValue="#FFFFFF"
   />
</KeyAttribute>

מוסיפים CustomAttribute בתוך KeyAttribute. CustomAttribute יוחל ב-framePosition שצוין על ידי KeyAttribute.

בתוך CustomAttribute צריך לציין attributeName וערך אחד להגדרה.

  • motion:attributeName הוא השם של המגדיר שאליו ייקרא המאפיין המותאם אישית הזה. בדוגמה הזו, תתבצע קריאה אל setColorFilter בתאריך Drawable.
  • motion:custom*Value הוא ערך מותאם אישית מהסוג שצוין בשם. בדוגמה הזו הערך המותאם אישית הוא צבע שצוין.

ערכים מותאמים אישית יכולים להיות מהסוגים הבאים:

  • צבע
  • מספר שלם
  • Float
  • מחרוזת
  • מאפיין
  • בוליאני

באמצעות ה-API הזה, MotionLayout יכול להוסיף אנימציה לכל דבר שמספק רכיב הגדרה בכל תצוגה.

רוצים לנסות?

  1. מפעילים שוב את האפליקציה ועוברים לשלב 6 כדי לראות את האנימציה בפעולה. כשלוחצים על הירח, הוא יעקוב אחרי הנתיב מתחילתו ועד סופו, דרך כל KeyAttribute שצוין בKeyFrameSet.

5fb6792126a09fda.gif

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

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

10. אפשר לגרור אירועים ונתיבים מורכבים

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

כדי לשלוט באנימציות עם נתיבים מורכבים באמצעות OnSwipe, כמו אנימציית הירח שבנית בשלבים האחרונים, נדרשת הבנה של אופן הפעולה של OnSwipe.

שלב 1: בדיקת ההתנהגות בזמן החלקה

  1. צריך לפתוח את xml/step7.xml ולחפש את ההצהרה הקיימת בנושא OnSwipe.

step7.xml

<!-- Fix OnSwipe by changing touchAnchorSide 

<OnSwipe
       motion:touchAnchorId="@id/moon"
       motion:touchAnchorSide="bottom"
/>
  1. מריצים את האפליקציה במכשיר ועוברים אל שלב 7. כדי ליצור אנימציה חלקה, אפשר לגרור את הירח לאורך נתיב הקשת.

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

ed96e3674854a548.gif

כדי להבין את הבאג, חשוב מה קורה כשהמשתמש נוגע ממש מתחת לחלק העליון של הקשת. מכיוון שלתג OnSwipe יש motion:touchAnchorSide="bottom", MotionLayout ינסה לשמור על מרחק קבוע בין האצבע לתחתית התצוגה במהלך האנימציה.

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

56cd575c5c77eddd.png

שלב 2: משתמשים בצד ימין

כדי להימנע מבאגים כאלה, חשוב לבחור תמיד touchAnchorId ו-touchAnchorSide שמתקדמים תמיד בכיוון אחד לאורך כל משך האנימציה.

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

עם זאת, גם bottom וגם top פנו בכיוון מסוים. כשOnSwipe ינסה לעקוב אחריו, הוא יתבלבל אם הכיוון ישתנה.

  1. כדי שהאנימציה הזו תעקוב אחר אירועי מגע, צריך לשנות את touchAnchorSide ל-right.

step7.xml

<!-- Fix OnSwipe by changing touchAnchorSide 

<OnSwipe
       motion:touchAnchorId="@id/moon"
       motion:touchAnchorSide="right"
/>

שלב 3: משתמשים ב- DragDirection

אפשר גם לשלב את dragDirection עם touchAnchorSide כדי ליצור מסלול צדדי שונה מהרגיל. עדיין חשוב ש-touchAnchorSide יתקדם רק בכיוון אחד, אבל אפשר לומר ל-MotionLayout באיזה כיוון לעקוב. לדוגמה, אפשר לשמור את touchAnchorSide="bottom", אבל להוסיף את dragDirection="dragRight". פעולה זו תגרום ל-MotionLayout לעקוב אחר המיקום של החלק התחתון של התצוגה, אבל להביא בחשבון רק את המיקום שלו כאשר זזים ימינה (היא מתעלמת מתנועה אנכית). לכן, גם אם החלק התחתון עולה ויורד, האנימציה עדיין תהיה תקינה עם OnSwipe.

  1. צריך לעדכן את OnSwipe כדי לעקוב בצורה נכונה אחר תנועת הירח.

step7.xml

<!-- Using dragDirection to control the direction of drag tracking 

<OnSwipe
       motion:touchAnchorId="@id/moon"
       motion:touchAnchorSide="bottom"
       motion:dragDirection="dragRight"
/>

רוצים לנסות?

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

5458dff382261427.gif

11. תנועת ריצה עם קוד

אפשר להשתמש ב-MotionLayout כדי ליצור אנימציות עשירות כשמשתמשים בהן עם CoordinatorLayout. בשלב הזה בונים כותרת ניתנת לכיווץ באמצעות MotionLayout.

שלב 1: בודקים את הקוד הקיים

  1. כדי להתחיל, צריך לפתוח את layout/activity_step8.xml.
  2. ב-layout/activity_step8.xml אפשר לראות שכבר נוצרו CoordinatorLayout ו-AppBarLayout תקינים.

activity_step8.xml

<androidx.coordinatorlayout.widget.CoordinatorLayout
       ...>
   <com.google.android.material.appbar.AppBarLayout
           android:id="@+id/appbar_layout"
           android:layout_width="match_parent"
           android:layout_height="180dp">
       <androidx.constraintlayout.motion.widget.MotionLayout
               android:id="@+id/motion_layout"
               ... >
           ...
       </androidx.constraintlayout.motion.widget.MotionLayout>
   </com.google.android.material.appbar.AppBarLayout>
  
   <androidx.core.widget.NestedScrollView
           ...
           motion:layout_behavior="@string/appbar_scrolling_view_behavior" >
           ...
   </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

בפריסה הזו נעשה שימוש ב-CoordinatorLayout כדי לשתף פרטי גלילה בין NestedScrollView לבין AppBarLayout. לכן, כשהערך של NestedScrollView נגלל למעלה, הוא יודיע ל-AppBarLayout על השינוי. כך מטמיעים סרגל כלים מתכווץ כמו זה ב-Android – גלילת הטקסט תהיה "מתואנת". עם הכותרת המתכווצת.

סצנת התנועה ש-@id/motion_layout מצביעה אליה דומה לסצנת התנועה בשלב האחרון. עם זאת, ההצהרה OnSwipe הוסרה כדי לאפשר לה לעבוד עם CoordinatorLayout.

  1. מפעילים את האפליקציה ועוברים לשלב 8. כשגוללים בטקסט, הירח לא זז.

שלב 2: גלילה ב-MotionLayout

  1. כדי שהתצוגה של MotionLayout תיגלל מיד לאחר הגלילה של NestedScrollView, צריך להוסיף את motion:minHeight ואת motion:layout_scrollFlags אל MotionLayout.

activity_step8.xml

<!-- Add minHeight and layout_scrollFlags to the MotionLayout -->

<androidx.constraintlayout.motion.widget.MotionLayout
       android:id="@+id/motion_layout"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       motion:layoutDescription="@xml/step8"
       motion:motionDebug="SHOW_PATH"
       android:minHeight="80dp"
       motion:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"  >
  1. מפעילים את האפליקציה שוב ועוברים לשלב 8. ה-MotionLayout מתכווץ ככל שגוללים למעלה. עם זאת, האנימציה עדיין לא מתקדמת בהתאם להתנהגות הגלילה.

שלב 3: מזיזים את התנועה בעזרת קוד

  1. פתיחת Step8Activity.kt . עורכים את הפונקציה coordinateMotion() כדי לדווח ל-MotionLayout על השינויים במיקום הגלילה.

Step8Activity.kt

// TODO: set progress of MotionLayout based on an AppBarLayout.OnOffsetChangedListener

private fun coordinateMotion() {
    val appBarLayout: AppBarLayout = findViewById(R.id.appbar_layout)
    val motionLayout: MotionLayout = findViewById(R.id.motion_layout)

    val listener = AppBarLayout.OnOffsetChangedListener { unused, verticalOffset ->
        val seekPosition = -verticalOffset / appBarLayout.totalScrollRange.toFloat()
        motionLayout.progress = seekPosition
    }

    appBarLayout.addOnOffsetChangedListener(listener)
}

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

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

רוצים לנסות?

  1. פורסים שוב את האפליקציה ומריצים את האנימציה שלב 8. אפשר לראות ש-MotionLayout יקדם את האנימציה על סמך מיקום הגלילה.

ee5ce4d9e33a59ca.gif

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

12. מזל טוב

ה-Codelab הזה מכסה את ה-API הבסיסי של MotionLayout.

כדי לראות דוגמאות נוספות של MotionLayout, אפשר לעיין בדוגמה הרשמית. בנוסף, חשוב לעיין במסמכי התיעוד.

מידע נוסף

MotionLayout תומך בעוד תכונות שלא נכללות ב-Codelab הזה, כמו KeyCycle, שמאפשר לשלוט בנתיבים או במאפיינים עם מחזורים חוזרים, ו-KeyTimeCycle, שמאפשר ליצור אנימציה לפי זמן השעון. עיינו בדוגמאות כדי לראות דוגמאות של כל אחד מהם.

לפרטים על קישורים לשיעורי Lab אחרים בקורס הזה, ראו דף הנחיתה של שיעורי Lab מתקדמים ל-Android ב-Kotlin.