أثناء استخدام تطبيقك، تظهر معلومات جديدة على الشاشة وتتم إزالة المعلومات القديمة. قد يكون تغيير المحتوى المعروض على الشاشة بشكل فوري أمرًا مزعجًا، وقد لا يلاحظ المستخدمون المحتوى الجديد الذي يظهر فجأة. تؤدي الصور المتحركة إلى إبطاء التغييرات وتلفت انتباه المستخدم من خلال الحركة، ما يجعل التحديثات أكثر وضوحًا.
هناك ثلاث صور متحركة شائعة يمكنك استخدامها لإظهار أو إخفاء طريقة عرض: صور متحركة للكشف عن المحتوى، وصور متحركة للتلاشي والظهور، وصور متحركة لقلب البطاقة.
إنشاء صورة متحركة تتضمّن تأثير الانتقال التدريجي
تتلاشى صورة متحركة ذات تأثير التلاشي التدريجي، والمعروفة أيضًا باسم التلاشي، تدريجيًا
View أو
ViewGroup، بينما تظهر صورة أخرى تدريجيًا في الوقت نفسه. تكون هذه الحركة مفيدة في الحالات التي تريد فيها تبديل المحتوى أو طرق العرض في تطبيقك. تستخدم حركة التلاشي والظهور المتقاطع الظاهرة هنا ViewPropertyAnimator، وهي متاحة لنظام التشغيل Android 3.1 (مستوى واجهة برمجة التطبيقات 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(); } } ... }
عند عرض وجه البطاقة الأمامي، يمكنك عرض وجهها الخلفي باستخدام الصورة المتحركة للقلب في الوقت المناسب. أنشئ طريقة لعرض الوجه الآخر من البطاقة، مع مراعاة ما يلي:
- تضبط هذه السمة الرسوم المتحركة المخصّصة التي أنشأتها لعمليات الانتقال بين الأجزاء.
- يستبدل هذا الإجراء الجزء المعروض بجزء جديد ويحرّك هذا الحدث باستخدام الصور المتحركة المخصّصة التي أنشأتها.
- تضيف هذه الطريقة الجزء الذي تم عرضه سابقًا إلى سجلّ الرجوع للأجزاء، وبالتالي عندما ينقر المستخدم على زر الرجوع، تعود البطاقة إلى وضعها السابق.
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، والتي تتوفّر في الإصدار 5.0 من نظام التشغيل Android (مستوى واجهة برمجة التطبيقات 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); }
في هذه الحالة، يتم ضبط نصف القطر الأولي لدائرة القص ليكون كبيرًا مثل العرض، وبالتالي يكون العرض مرئيًا قبل بدء الحركة. تم ضبط نصف القطر النهائي على صفر حتى يتم إخفاء طريقة العرض عند انتهاء الحركة.
أضِف أداة معالجة إلى الصورة المتحركة حتى يمكن ضبط مستوى ظهور العرض على INVISIBLE عند اكتمال الصورة المتحركة.
مراجع إضافية
- الصور المتحركة باستخدام Jetpack Compose
- الإيماءات باستخدام Jetpack Compose