تواصل Google تحسين حياتنا من خلال إصدار مكتبات مفيدة وواجهات برمجة التطبيقات. من بينها كان MotionLayout الجديد. بالنظر إلى وفرة الرسوم المتحركة في تطبيقاتنا ، قام زميلي سيدريك هولتز بتنفيذ الرسوم المتحركة الأكثر أهمية في تطبيقنا - التصويت في المواعدة - باستخدام واجهة برمجة التطبيقات الجديدة ، مع توفير قدر كبير من الشفرة. أشارك ترجمة مقاله.انتهى مؤتمر Google I / O 2019 مؤخرًا ، حيث تم الإعلان عن التحديثات وأحدث التحسينات على SDK الحبيب. شخصياً ، كنت مهتمًا بشكل خاص بالعرض التقديمي الذي قدمه نيكولاس رود وجون هوفورد حول الوظيفة المستقبلية لـ ConstraintLayout. بشكل أكثر تحديدًا ، حول توسعها في شكل MotionLayout.
بعد إصدار الإصدار التجريبي ، أردت تنفيذ رسوم متحركة على أساس هذه المكتبة.
أولاً ، دعنا نحدد المصطلحات:
"MotionLayout عبارة عن ConstraintLayout التي تسمح لك بتنشيط المخططات بين الولايات المختلفة." - الوثائق
إذا لم تكن قد قرأت
سلسلة من المقالات التي كتبها Nicholas Road والتي تشرح الأفكار الرئيسية لـ MotionLayout ، فإنني أوصي بشدة بقراءتها.
لذا ، مع المقدمة ، لنرى الآن ما نريد الحصول عليه:

كومة بطاقة
نظهر الخريطة تحول
للبدء ، أضف MotionLayout إلى دليل التخطيط ، والذي يحتوي حتى الآن على بطاقة واحدة فقط:
<androidx.constraintlayout.motion.widget.MotionLayout android:id="@+id/motionLayout" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutDescription="@xml/scene_swipe" app:motionDebug="SHOW_ALL"> <FrameLayout android:id="@+id/topCard" android:layout_width="0dp" android:layout_height="0dp" /> </androidx.constraintlayout.motion.widget.MotionLayout>
انتبه لهذا السطر: app: motionDebug = "SHOW_ALL". إنها تتيح لنا عرض معلومات تصحيح الأخطاء ، مسار الكائنات ، الحالات مع بداية ونهاية الحركة ، بالإضافة إلى التقدم الحالي. يساعد الخط كثيرًا عند تصحيح الأخطاء ، ولكن لا تنسَ حذفه قبل إرساله إلى المنتج: لا يوجد تذكير بهذا.
كما ترون ، لم نضع أي قيود للمشاهدة هنا. سيتم نقلها من المشهد (MotionScene) ، والتي نحددها الآن.
لنبدأ بتحديد الحالة الأولية: توجد بطاقة واحدة في وسط الشاشة ، مع وجود مسافات بادئة.
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <ConstraintSet android:id="@+id/rest"> <Constraint android:id="@id/topCard" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="50dp" android:layout_marginEnd="50dp" android:layout_marginStart="50dp" android:layout_marginTop="50dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> </ConstraintSet> </MotionScene>
إضافة مجموعات القيد (ConstraintSet) تمرير وما شابه ذلك. سيعكسون حالة البطاقة العلوية عند إزاحتها تمامًا إلى اليسار أو اليمين. نريد أن تتوقف الخريطة قبل أن تختفي من الشاشة لإظهار رسوم متحركة جميلة تؤكد قرارنا.
<ConstraintSet android:id="@+id/pass" app:deriveConstraintsFrom="@+id/rest"> <Constraint android:id="@id/topCard" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginBottom="80dp" android:layout_marginEnd="200dp" android:layout_marginStart="50dp" android:layout_marginTop="20dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintWidth_percent="0.7" /> </ConstraintSet> <ConstraintSet android:id="@+id/like" app:deriveConstraintsFrom="@id/rest"> <Constraint android:id="@id/topCard" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginBottom="80dp" android:layout_marginEnd="50dp" android:layout_marginStart="200dp" android:layout_marginTop="20dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintWidth_percent="0.7" /> </ConstraintSet>
أضف مجموعتي القيود إلى المشهد السابق. أنها متطابقة تقريبا ، معكوسة فقط على جانبي الشاشة.
الآن لدينا ثلاث مجموعات من القيود - البدء ، مثل ، وتمرير. دعنا نعرّف الانتقال بين هذه الحالات.
للقيام بذلك ، قم بإضافة انتقال واحد إلى اليسار للتمرير السريع ، والآخر إلى اليمين للتمرير السريع.
<Transition app:constraintSetEnd="@+id/pass" app:constraintSetStart="@+id/rest" app:duration="300"> <OnSwipe app:dragDirection="dragLeft" app:onTouchUp="autoComplete" app:touchAnchorId="@id/topCard" app:touchAnchorSide="left" app:touchRegionId="@id/topCard" /> </Transition> <Transition app:constraintSetEnd="@+id/like" app:constraintSetStart="@+id/rest" app:duration="300"> <OnSwipe app:dragDirection="dragRight" app:onTouchUp="autoComplete" app:touchAnchorId="@+id/topCard" app:touchAnchorSide="right" app:touchRegionId="@id/topCard" /> </Transition>
لذلك ، بالنسبة للبطاقة العليا ، قمنا بتعيين حركة التمرير السريع على اليسار ونفس الشيء - مرآة التمرير السريع إلى اليمين.
ستساعد هذه الخصائص في تحسين التفاعل مع مشهدنا:
- touchRegionId: نظرًا لأننا قمنا بإضافة الحشو حول الخريطة ، فإننا نحتاج إلى التأكد من أن اللمس يتم التعرف عليه فقط في منطقة الخريطة نفسها ، وليس في MotionLayout بالكامل. يمكن القيام بذلك باستخدام touchRegionId.
- onTouchUp: ماذا سيحدث للرسوم المتحركة بعد إصدار البطاقة؟ يجب أن تنتقل أو تعود إلى حالتها الأولية ، بحيث يكون الإكمال التلقائي قابل للتطبيق.
دعونا نرى ما حدث:

تنفصل الخريطة تلقائيًا عن الشاشة
الآن سنعمل على الرسوم المتحركة ، والتي ستبدأ عندما تنفصل الخريطة عن الشاشة.
نضيف مجموعتين من مجموعات ConstraintSet لكل حالة نهائية من رسومنا المتحركة: تترك الخريطة الشاشة إلى اليسار واليمين.
في الأمثلة التالية ، سأوضح كيفية إنشاء الحالة المشابهة ، وستكرر حالة المرور هذه النسخ المتطابق. مثال عملي يمكن رؤيته بالكامل في
المستودع .
<ConstraintSet android:id="@+id/offScreenLike"> <Constraint android:id="@id/topCard" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginBottom="80dp" android:layout_marginEnd="50dp" android:layout_marginTop="20dp" app:layout_constraintStart_toEndOf="parent" app:layout_constraintWidth_percent="0.7" /> </ConstraintSet>
الآن ، كما في المثال السابق ، تحتاج إلى تحديد الانتقال من حالة التمرير السريع إلى الحالة النهائية. يجب أن يعمل الانتقال تلقائيًا بعد الرسوم المتحركة للتمرير السريع. يمكن القيام بذلك باستخدام النقل التلقائي:
<Transition app:autoTransition="animateToEnd" app:constraintSetEnd="@+id/offScreenLike" app:constraintSetStart="@+id/like" app:duration="150" />
الآن لدينا بطاقة ممغنطة يمكن تمريرها من الشاشة!

أسفل الخريطة الرسوم المتحركة
الآن دعونا نجعل البطاقة السفلية لخلق وهم اللانهاية من سطح السفينة.
أضف خريطة أخرى إلى التخطيط ، على غرار المخطط الأول:
<FrameLayout android:id="@+id/bottomCard" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/colorAccent" />
قم بتغيير XML لتعيين القيود التي تنطبق على هذه الخريطة في كل مرحلة من مراحل الرسوم المتحركة:
<ConstraintSet android:id="@id/rest"> <!-- ... --> <Constraint android:id="@id/bottomCard"> <Layout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="50dp" android:layout_marginEnd="50dp" android:layout_marginStart="50dp" android:layout_marginTop="50dp" /> <Transform android:scaleX="0.90" android:scaleY="0.90" /> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/offScreenLike" app:deriveConstraintsFrom="@id/like"> <!-- ... --> <Constraint android:id="@id/bottomCard"> <Transform android:scaleX="1" android:scaleY="1" /> </Constraint> </ConstraintSet>
للقيام بذلك ، يمكننا استخدام خاصية ConstraintSet المريحة.
بشكل افتراضي ، تأخذ كل مجموعة جديدة سمات من الأصل MotionLayout. ولكن باستخدام علامة deriveConstrictFrom ، يمكنك تعيين أصل آخر لمجموعتنا. يجب أن يؤخذ في الاعتبار أننا إذا وضعنا قيودًا باستخدام علامة القيد ، فسنعيد تحديد جميع القيود من المجموعة الأصل. لتجنب ذلك ، يمكنك تعيين سمات محددة في
العلامات بحيث يتم استبدالها فقط.

في حالتنا ، هذا يعني أننا في مجموعة المرور لا نحدد علامة Layout ، ولكننا ننسخها من الأصل. ومع ذلك ، فإننا نتجاوز Transform ، وبالتالي ، فإننا نستبدل جميع السمات المحددة في علامة Transform بعلاماتنا ، في هذه الحالة ، تغيير في الحجم.
هذا هو مدى سهولة استخدام MotionLayout لإضافة عنصر جديد ودمجه بسهولة مع الرسوم المتحركة لمشهدنا.

جعل الرسوم المتحركة لا نهاية لها
بعد اكتمال الرسوم المتحركة ، لا يمكن تمرير البطاقة العلوية بعيدًا ، لأنها أصبحت الآن بطاقة أسفل. للحصول على الرسوم المتحركة التي لا نهاية لها ، تحتاج إلى تبديل البطاقات.
في البداية كنت أرغب في القيام بذلك مع انتقال جديد:
<Transition app:autoTransition="jumpToEnd" app:constraintSetEnd="@+id/rest" app:constraintSetStart="@+id/offScreenLike" app:duration="0" />

الرسوم المتحركة بأكملها يلعب كما يجب. الآن لدينا كومة من البطاقات التي يمكنك تمريرها إلى ما لا نهاية!
بعد قليل انتقد ، لاحظت شيئا. يتوقف الانتقال إلى نهاية الرسوم المتحركة عند لمس البطاقة. على الرغم من أن مدة الرسوم المتحركة هي صفر ، فإنها لا تزال تتوقف ، وهو أمر سيء.

تمكنت من الفوز في طريقة واحدة فقط - عن طريق تغيير الانتقال النشط في MotionLayout برمجياً.
للقيام بذلك ، سنقوم بتعيين رد الاتصال عند الانتهاء من الرسوم المتحركة. بمجرد اكتمال offScreenLike و offScreenPass ، فإننا ببساطة نعيد ضبط الانتقال إلى حالة الراحة ونفقد التقدم.
motionLayout.setTransitionListener(object : TransitionAdapter() { override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) { when (currentId) { R.id.offScreenPass, R.id.offScreenLike -> { motionLayout.progress = 0f motionLayout.setTransition(R.id.rest, R.id.like) } } } })
لا يهم الانتقال الذي نضعه أو نمره أو نوده ، عندما ننتقل إلى التحول المطلوب.

يبدو هو نفسه ، لكن الرسوم المتحركة لا تتوقف! دعنا ننتقل!
ربط البيانات
إنشاء بيانات اختبار لعرضها على الخرائط. في الوقت الحالي ، سنقتصر على تغيير لون خلفية كل بطاقة.
نقوم بإنشاء ViewModel باستخدام طريقة svayp التي تستبدل فقط البيانات الجديدة. قم بربطه بالنشاط بهذه الطريقة:
val viewModel = ViewModelProviders .of(this) .get(SwipeRightViewModel::class.java) viewModel .modelStream .observe(this, Observer { bindCard(it) }) motionLayout.setTransitionListener(object : TransitionAdapter() { override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) { when (currentId) { R.id.offScreenPass, R.id.offScreenLike -> { motionLayout.progress = 0f motionLayout.setTransition(R.id.rest, R.id.like) viewModel.swipe() } } } })
يبقى لإبلاغ ViewModel عن الانتهاء من الرسوم المتحركة السريعة ، وسيتم تحديث البيانات التي يتم عرضها حاليًا.

الرموز المنبثقة
أضف منظرين ، عندما يظهر التمرير السريع على أحد جانبي الشاشة (يظهر واحد فقط أدناه ، يتم عكس الثاني).
<ImageView android:id="@+id/likeIndicator" android:layout_width="0dp" android:layout_height="0dp" />
الآن للخرائط ، تحتاج إلى تعيين حالات الرسوم المتحركة مع هذه المشاهدات.
<ConstraintSet android:id="@id/rest"> <!-- ... --> <Constraint android:id="@+id/like"> <Layout android:layout_width="40dp" android:layout_height="40dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Transform android:scaleX="0.5" android:scaleY="0.5" /> <PropertySet android:alpha="0" /> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/like" app:deriveConstraintsFrom="@id/rest"> <!-- ... --> <Constraint android:id="@+id/like"> <Layout android:layout_width="100dp" android:layout_height="100dp" app:layout_constraintBottom_toBottomOf="@id/topCard" app:layout_constraintEnd_toEndOf="@id/topCard" app:layout_constraintStart_toStartOf="@id/topCard" app:layout_constraintTop_toTopOf="@id/topCard" /> <Transform android:scaleX="1" android:scaleY="1" /> <PropertySet android:alpha="1" /> </Constraint> </ConstraintSet>
ليست هناك حاجة لوضع قيود على الرسوم المتحركة التي تتجاوز الشاشة ، لأنها موروثة من الآباء والأمهات. وفي حالتنا ، هذه حالة انتقاد.
هذا هو كل ما نحتاج إلى القيام به. يمكنك الآن إضافة مكونات إلى سلاسل الرسوم المتحركة بسهولة شديدة.

تشغيل الرسوم المتحركة برمجيا
يمكننا أن نجعل زرين على البطاقات حتى لا يتمكن المستخدم من التمرير فقط ، ولكن التحكم في استخدام الأزرار.
يبدأ كل زر في نفس الرسوم المتحركة مثل التمرير.
كالعادة ، اشترك في نقرات الأزرار وابدأ الرسوم المتحركة مباشرة على كائن MotionLayout:
likeButton.setOnClickListener { motionLayout.transitionToState(R.id.like) } passButton.setOnClickListener { motionLayout.transitionToState(R.id.pass) }
نحتاج إلى إضافة أزرار إلى كل من البطاقات العلوية والسفلية حتى يتم تشغيل الرسوم المتحركة بشكل مستمر. ومع ذلك ، بالنسبة للخريطة السفلية ، لا يلزم النقر فوق الاشتراك ، لأنه إما غير مرئي ، أو أن الخريطة العلوية متحركة ، ولا نريد مقاطعتها.

مثال آخر رائع على كيفية تعامل MotionLayout مع التغييرات التي تطرأ على الحالة بالنسبة لنا. دعونا تبطئ الرسوم المتحركة قليلاً:

انظر إلى المرحلة الانتقالية التي ينفذها MotionLayout عندما يستبدل النجاح. السحر!
اسحب الخريطة على طول المنحنى
لنفترض أننا أعجبنا إذا كانت الخريطة لا تتحرك في خط مستقيم ، ولكن في منحنى (بصراحة ، أردت فقط محاولة القيام بذلك).
ثم تحتاج إلى تحديد KeyPosition للحركة في كلا الاتجاهين ، بحيث يكون مسار الحركة مقوسًا بواسطة قوس.
أضف هذا إلى مشهد الحركة:
<Transition app:constraintSetEnd="@+id/like" app:constraintSetStart="@+id/rest" app:duration="300"> <!-- ... --> <KeyFrameSet> <KeyPosition app:drawPath="path" app:framePosition="50" app:keyPositionType="pathRelative" app:motionTarget="@id/topCard" app:percentX="0.5" app:percentY="-0.1" /> </KeyFrameSet> </Transition>

الآن تتحرك الخريطة على طول طريق منحني غير عادي. السحرية!
استنتاج
عندما تقارن مقدار الشفرة التي حصلت عليها عند إنشاء هذه الرسوم المتحركة مع تطبيقنا الحالي للرسوم المتحركة المشابهة في الإنتاج ، تكون النتيجة مذهلة.
تتعامل MotionLayout بشكل غير محسوس مع إلغاء الانتقالات (على سبيل المثال ، عند لمسها) ، وإنشاء سلاسل للرسوم المتحركة ، وتغيير الخصائص أثناء الانتقالات ، وغير ذلك الكثير. تعمل هذه الأداة على تغيير كل شيء بشكل أساسي ، مما يؤدي إلى تبسيط منطق واجهة المستخدم إلى حد كبير.
هناك بعض الأشياء الأخرى التي تستحق العمل عليها (معظمها تعطيل الرسوم المتحركة والتمرير ثنائي الاتجاه في RecyclerView) ، لكنني متأكد من أن هذا قابل للحل.
تذكر أن المكتبة لا تزال في حالة تجريبية ، لكنها تفتح بالفعل العديد من الفرص المثيرة لنا. نحن نتطلع إلى إصدار MotionLayout ، الذي ، وأنا متأكد ، سيكون مفيدًا أكثر من مرة واحدة في المستقبل. يمكنك رؤية تطبيق العمل بالكامل من هذه المقالة في
المستودع .
ملاحظة: نظرًا لأنهم منحوني الكلمة كمترجم ، فإن فريق Android لدينا لديه مكان للمطور . شكرا لاهتمامكم