בזמן השימוש באפליקציה, מידע חדש מופיע על המסך ומידע ישן מוסר. שינוי מיידי של מה שמוצג במסך עלול להיות מטריד, והמשתמשים עלולים לפספס תוכן חדש שמופיע פתאום. האנימציות מאטות את השינויים ומושכות את העין של המשתמש באמצעות תנועה, כך שהעדכונים בולטים יותר.
יש שלוש אנימציות נפוצות שאפשר להשתמש בהן כדי להציג או להסתיר תצוגה: אנימציות של חשיפה, אנימציות של מעבר הדרגתי ואנימציות של היפוך כרטיס.
יצירת אנימציית מעבר הדרגתי
אנימציית מעבר הדרגתי – שנקראת גם התפוגגות – גורמת לView או לViewGroup אחד להתפוגג בהדרגה, ובמקביל גורמת לאלמנט אחר להופיע בהדרגה. האנימציה הזו שימושית במצבים שבהם רוצים להחליף תוכן או תצוגות באפליקציה. אנימציית המעבר ההדרגתי שמוצגת כאן משתמשת ב-ViewPropertyAnimator, שזמין ב-Android 3.1 (רמת API 12) ומעלה.
דוגמה למעבר הדרגתי מאינדיקטור התקדמות לתוכן טקסט:
יצירת התצוגות
יוצרים את שני התצוגות שרוצים ליצור ביניהן מעבר הדרגתי. בדוגמה הבאה נוצר מחוון התקדמות ותצוגת טקסט שאפשר לגלול בה:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView style="?android:textAppearanceMedium"
android:lineSpacingMultiplier="1.2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/lorem_ipsum"
android:padding="16dp" />
</ScrollView>
<ProgressBar android:id="@+id/loading_spinner"
style="?android:progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
הגדרת אנימציית מעבר הדרגתי
כדי להגדיר את אנימציית ההחלפה ההדרגתית:
- יוצרים משתני חברים לתצוגות שרוצים ליצור ביניהן מעבר הדרגתי. תצטרכו את ההפניות האלה בהמשך, כשמשנים את התצוגות במהלך האנימציה.
- מגדירים את החשיפה של התצוגה שמופיעה בהדרגה לערך
GONE. כך התצוגה לא תשתמש במקום בפריסה ולא תיכלל בחישובי הפריסה, מה שיזרז את העיבוד. - שמירת מאפיין המערכת
config_shortAnimTimeבמשתנה חבר. המאפיין הזה מגדיר משך זמן 'קצר' רגיל לאנימציה. המשך הזה אידיאלי לאנימציות עדינות או לאנימציות שמתרחשות לעיתים קרובות. אפשר גם להשתמש ב-config_longAnimTimeוב-config_mediumAnimTime.
הנה דוגמה לשימוש בפריסה מקטע הקוד הקודם כתצוגת תוכן של הפעילות:
Kotlin
class CrossfadeActivity : Activity() { private lateinit var contentView: View private lateinit var loadingView: View private var shortAnimationDuration: Int = 0 ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_crossfade) contentView = findViewById(R.id.content) loadingView = findViewById(R.id.loading_spinner) // Initially hide the content view. contentView.visibility = View.GONE // Retrieve and cache the system's default "short" animation time. shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime) } ... }
Java
public class CrossfadeActivity extends Activity { private View contentView; private View loadingView; private int shortAnimationDuration; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_crossfade); contentView = findViewById(R.id.content); loadingView = findViewById(R.id.loading_spinner); // Initially hide the content view. contentView.setVisibility(View.GONE); // Retrieve and cache the system's default "short" animation time. shortAnimationDuration = getResources().getInteger( android.R.integer.config_shortAnimTime); } ... }
עמעום הדרגתי של התצוגות
כדי ליצור מעבר הדרגתי בין התצוגות, צריך להגדיר אותן בצורה נכונה:
- בתצוגה שמופיעה בהדרגה, מגדירים את ערך האלפא ל-0 ואת ההגדרה של הנראות
ל-
VISIBLEמההגדרה הראשונית שלהGONE. התצוגה תהיה גלויה אבל שקופה. - בתצוגה שמופיעה בהדרגה, מפעילים אנימציה של ערך האלפא מ-0 עד 1. בתצוגה שמונפשת החוצה, מנפישים את ערך האלפא מ-1 ל-0.
- באמצעות התג
onAnimationEnd()ב-Animator.AnimatorListener, מגדירים את החשיפה של התצוגה שמופיעה בהדרגה ל-GONE. למרות שערך האלפא הוא 0, הגדרת הנראות של התצוגה ל-GONEמונעת מהתצוגה להשתמש במקום בפריסה, והיא לא נכללת בחישובי הפריסה, מה שמזרז את העיבוד.
בדוגמה הבאה אפשר לראות איך עושים את זה:
Kotlin
class CrossfadeActivity : Activity() { private lateinit var contentView: View private lateinit var loadingView: View private var shortAnimationDuration: Int = 0 ... private fun crossfade() { contentView.apply { // Set the content view to 0% opacity but visible, so that it is // visible but fully transparent during the animation. alpha = 0f visibility = View.VISIBLE // Animate the content view to 100% opacity and clear any animation // listener set on the view. animate() .alpha(1f) .setDuration(shortAnimationDuration.toLong()) .setListener(null) } // Animate the loading view to 0% opacity. After the animation ends, // set its visibility to GONE as an optimization step so it doesn't // participate in layout passes. loadingView.animate() .alpha(0f) .setDuration(shortAnimationDuration.toLong()) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { loadingView.visibility = View.GONE } }) } }
Java
public class CrossfadeActivity extends Activity { private View contentView; private View loadingView; private int shortAnimationDuration; ... private void crossfade() { // Set the content view to 0% opacity but visible, so that it is // visible but fully transparent during the animation. contentView.setAlpha(0f); contentView.setVisibility(View.VISIBLE); // Animate the content view to 100% opacity and clear any animation // listener set on the view. contentView.animate() .alpha(1f) .setDuration(shortAnimationDuration) .setListener(null); // Animate the loading view to 0% opacity. After the animation ends, // set its visibility to GONE as an optimization step so it doesn't // participate in layout passes. loadingView.animate() .alpha(0f) .setDuration(shortAnimationDuration) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { loadingView.setVisibility(View.GONE); } }); } }
יצירת אנימציה של כרטיס מתהפך
האנימציה של כרטיס מתהפך מאפשרת לעבור בין תצוגות של תוכן. אנימציית היפוך הכרטיס שמוצגת כאן משתמשת ב-FragmentTransaction.
כך נראה היפוך של כרטיס:
יצירת אובייקטים של אנימטור
כדי ליצור את אנימציית היפוך הכרטיס, צריך ארבעה אנימטורים. שני אנימטורים הם לאנימציה של החלק הקדמי של הכרטיס החוצה ולצד שמאל, ולאנימציה של הכרטיס פנימה ומשמאל. שני האנימטורים האחרים הם לאנימציה של גב הכרטיס שנכנסת מימין ויוצאת ימינה.
card_flip_left_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="-180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Halfway through the rotation, set the alpha to 1. See startOffset. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_left_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rotate. -->
<objectAnimator
android:valueFrom="0"
android:valueTo="180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Halfway through the rotation, set the alpha to 0. See startOffset. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_right_in.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Before rotating, immediately set the alpha to 0. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:duration="0" />
<!-- Rotate. -->
<objectAnimator
android:valueFrom="180"
android:valueTo="0"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Halfway through the rotation, set the alpha to 1. See startOffset. -->
<objectAnimator
android:valueFrom="0.0"
android:valueTo="1.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
card_flip_right_out.xml
<set xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Rotate. -->
<objectAnimator
android:valueFrom="0"
android:valueTo="-180"
android:propertyName="rotationY"
android:interpolator="@android:interpolator/accelerate_decelerate"
android:duration="@integer/card_flip_time_full" />
<!-- Halfway through the rotation, set the alpha to 0. See startOffset. -->
<objectAnimator
android:valueFrom="1.0"
android:valueTo="0.0"
android:propertyName="alpha"
android:startOffset="@integer/card_flip_time_half"
android:duration="1" />
</set>
יצירת התצוגות
כל צד של הכרטיס הוא פריסה נפרדת שיכולה להכיל כל תוכן שרוצים, כמו שני תצוגות טקסט, שתי תמונות או כל שילוב של תצוגות שאפשר לעבור ביניהן. משתמשים בשני הפריסות בקטעים שיונפשו בהמשך. הפריסה הבאה יוצרת צד אחד של כרטיס, שמוצג בו טקסט:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#a6c"
android:padding="16dp"
android:gravity="bottom">
<TextView android:id="@android:id/text1"
style="?android:textAppearanceLarge"
android:textStyle="bold"
android:textColor="#fff"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/card_back_title" />
<TextView style="?android:textAppearanceSmall"
android:textAllCaps="true"
android:textColor="#80ffffff"
android:textStyle="bold"
android:lineSpacingMultiplier="1.2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/card_back_description" />
</LinearLayout>
בפריסה הבאה נוצר הצד השני של הכרטיס, שבו מוצג ImageView:
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/image1"
android:scaleType="centerCrop"
android:contentDescription="@string/description_image_1" />
יצירת הפרגמנטים
יוצרים מחלקות של קטעים לחלק הקדמי והאחורי של הכרטיס. במחלקות המקטעים, מחזירים את הפריסות שיצרתם מהשיטה onCreateView(). לאחר מכן אפשר ליצור מופעים של הקטע הזה בפעילות האב, במקום שבו רוצים להציג את הכרטיס.
בדוגמה הבאה מוצגות מחלקות של קטעי קוד מוטמעים בתוך פעילות האב שמשתמשת בהן:
Kotlin
class CardFlipActivity : FragmentActivity() { ... /** * A fragment representing the front of the card. */ class CardFrontFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate(R.layout.fragment_card_front, container, false) } /** * A fragment representing the back of the card. */ class CardBackFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate(R.layout.fragment_card_back, container, false) } }
Java
public class CardFlipActivity extends FragmentActivity { ... /** * A fragment representing the front of the card. */ public class CardFrontFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_front, container, false); } } /** * A fragment representing the back of the card. */ public class CardBackFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_back, container, false); } } }
הנפשת היפוך הכרטיס
הצגת הפרגמנטים בתוך פעילות אב. כדי לעשות את זה, יוצרים את הפריסה של הפעילות. בדוגמה הבאה נוצר FrameLayout שאפשר להוסיף לו פרגמנטים בזמן הריצה:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
בקוד הפעילות, מגדירים את תצוגת התוכן להיות הפריסה שיוצרים. מומלץ להציג קטע ברירת מחדל כשהפעילות נוצרת. בדוגמה הבאה של פעילות אפשר לראות איך להציג את החלק הקדמי של הכרטיס כברירת מחדל:
Kotlin
class CardFlipActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_activity_card_flip) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .add(R.id.container, CardFrontFragment()) .commit() } } ... }
Java
public class CardFlipActivity extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_activity_card_flip); if (savedInstanceState == null) { getSupportFragmentManager() .beginTransaction() .add(R.id.container, new CardFrontFragment()) .commit(); } } ... }
אפשר להציג את הצד האחורי של הכרטיס עם אנימציית ההיפוך בזמן המתאים, כשהצד הקדמי של הכרטיס מוצג. יוצרים מתודה להצגת הצד השני של הכרטיס, שעושה את הפעולות הבאות:
- הגדרת האנימציות המותאמות אישית שיצרתם למעברים בין רכיבי ה-Fragment.
- הפעולה הזו מחליפה את הפריט המוצג בפריט חדש ומפעילה את האנימציות המותאמות אישית שיצרתם.
- הפעולה מוסיפה את מקטע ה-Fragment שהוצג קודם למקבץ הפעילויות הקודמות (back stack) של ה-Fragment, כך שכשהמשתמש מקיש על הכפתור "הקודם", הכרטיס מתהפך חזרה.
Kotlin
class CardFlipActivity : FragmentActivity() { ... private fun flipCard() { if (showingBack) { supportFragmentManager.popBackStack() return } // Flip to the back. showingBack = true // Create and commit a new fragment transaction that adds the fragment // for the back of the card, uses custom animations, and is part of the // fragment manager's back stack. supportFragmentManager.beginTransaction() // Replace the default fragment animations with animator // resources representing rotations when switching to the back // of the card, as well as animator resources representing // rotations when flipping back to the front, such as when the // system Back button is tapped. .setCustomAnimations( R.animator.card_flip_right_in, R.animator.card_flip_right_out, R.animator.card_flip_left_in, R.animator.card_flip_left_out ) // Replace any fragments in the container view with a fragment // representing the next page, indicated by the just-incremented // currentPage variable. .replace(R.id.container, CardBackFragment()) // Add this transaction to the back stack, letting users press // the Back button to get to the front of the card. .addToBackStack(null) // Commit the transaction. .commit() } }
Java
public class CardFlipActivity extends FragmentActivity { ... private void flipCard() { if (showingBack) { getSupportFragmentManager().popBackStack(); return; } // Flip to the back. showingBack = true; // Create and commit a new fragment transaction that adds the fragment // for the back of the card, uses custom animations, and is part of the // fragment manager's back stack. getSupportFragmentManager() .beginTransaction() // Replace the default fragment animations with animator // resources representing rotations when switching to the back // of the card, as well as animator resources representing // rotations when flipping back to the front, such as when the // system Back button is pressed. .setCustomAnimations( R.animator.card_flip_right_in, R.animator.card_flip_right_out, R.animator.card_flip_left_in, R.animator.card_flip_left_out) // Replace any fragments in the container view with a fragment // representing the next page, indicated by the just-incremented // currentPage variable. .replace(R.id.container, new CardBackFragment()) // Add this transaction to the back stack, letting users press // Back to get to the front of the card. .addToBackStack(null) // Commit the transaction. .commit(); } }
יצירת אנימציית חשיפה מעגלית
אנימציות של חשיפה מספקות למשתמשים רצף חזותי כשמציגים או מסתירים קבוצה של רכיבי ממשק משתמש. השיטה
ViewAnimationUtils.createCircularReveal()
מאפשרת להנפיש עיגול גזירה כדי לחשוף או להסתיר תצוגה. האנימציה הזו מסופקת במחלקה ViewAnimationUtils, שזמינה ב-Android מגרסה 5.0 (רמת API 21) ואילך.
דוגמה להצגת תצוגה שהייתה בלתי נראית:
Kotlin
// A previously invisible view. val myView: View = findViewById(R.id.my_view) // Check whether the runtime version is at least Android 5.0. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Get the center for the clipping circle. val cx = myView.width / 2 val cy = myView.height / 2 // Get the final radius for the clipping circle. val finalRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat() // Create the animator for this view. The start radius is 0. val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius) // Make the view visible and start the animation. myView.visibility = View.VISIBLE anim.start() } else { // Set the view to invisible without a circular reveal animation below // Android 5.0. myView.visibility = View.INVISIBLE }
Java
// A previously invisible view. View myView = findViewById(R.id.my_view); // Check whether the runtime version is at least Android 5.0. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Get the center for the clipping circle. int cx = myView.getWidth() / 2; int cy = myView.getHeight() / 2; // Get the final radius for the clipping circle. float finalRadius = (float) Math.hypot(cx, cy); // Create the animator for this view. The start radius is 0. Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius); // Make the view visible and start the animation. myView.setVisibility(View.VISIBLE); anim.start(); } else { // Set the view to invisible without a circular reveal animation below // Android 5.0. myView.setVisibility(View.INVISIBLE); }
האנימציה ViewAnimationUtils.createCircularReveal() כוללת חמישה פרמטרים.
הפרמטר הראשון הוא התצוגה שרוצים להסתיר או להציג במסך. שני הפרמטרים הבאים הם הקואורדינטות X ו-Y של מרכז העיגול לחיתוך. בדרך כלל, זה מרכז התצוגה, אבל אפשר גם להשתמש בנקודה שהמשתמש מקיש עליה כדי שהאנימציה תתחיל במקום שהוא בחר. הפרמטר הרביעי הוא הרדיוס ההתחלתי של עיגול החיתוך.
בדוגמה הקודמת, הרדיוס ההתחלתי מוגדר לאפס כדי שהתצוגה שמוצגת תוסתר על ידי העיגול. הפרמטר האחרון הוא הרדיוס הסופי של העיגול. כשמציגים תצוגה, צריך להגדיל את הרדיוס הסופי כך שיהיה גדול יותר מהתצוגה, כדי שהתצוגה תיחשף במלואה לפני שהאנימציה מסתיימת.
כדי להסתיר תצוגה שהייתה גלויה קודם:
Kotlin
// A previously visible view. val myView: View = findViewById(R.id.my_view) // Check whether the runtime version is at least Android 5.0. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Get the center for the clipping circle. val cx = myView.width / 2 val cy = myView.height / 2 // Get the initial radius for the clipping circle. val initialRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat() // Create the animation. The final radius is 0. val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f) // Make the view invisible when the animation is done. anim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) myView.visibility = View.INVISIBLE } }) // Start the animation. anim.start() } else { // Set the view to visible without a circular reveal animation below // Android 5.0. myView.visibility = View.VISIBLE }
Java
// A previously visible view. final View myView = findViewById(R.id.my_view); // Check whether the runtime version is at least Android 5.0. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Get the center for the clipping circle. int cx = myView.getWidth() / 2; int cy = myView.getHeight() / 2; // Get the initial radius for the clipping circle. float initialRadius = (float) Math.hypot(cx, cy); // Create the animation. The final radius is 0. Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f); // Make the view invisible when the animation is done. anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); myView.setVisibility(View.INVISIBLE); } }); // Start the animation. anim.start(); } else { // Set the view to visible without a circular reveal animation below Android // 5.0. myView.setVisibility(View.VISIBLE); }
במקרה הזה, הרדיוס הראשוני של עיגול החיתוך מוגדר להיות גדול כמו התצוגה, כדי שהתצוגה תהיה גלויה לפני שהאנימציה מתחילה. הרדיוס הסופי מוגדר לאפס כדי שהתצוגה תוסתר כשהאנימציה תסתיים.
מוסיפים listener לאנימציה כדי שאפשר יהיה להגדיר את הנראות של התצוגה ל-INVISIBLE כשהאנימציה מסתיימת.