مدخل إلى التحريك في React Native


جميل بيلوني

يعد التحريك باستخدام مكتبة React Native من المواضيع الأساسية المطروحة في أقسام الدراسة أو ورشات العمل، ربما لكون عدد من المطورين يجدون تحديًا في استخدامها. في الوقت الذي تركّز فيه الكثير من المدونات والمصادر في الويب على جوانب الأداء عند استخدام React Native، لا يخوض إلا عدد قليل منها في الأساسيات. سنعرض في هذه المقالة لأساسيّات تطبيق التحريكات باستخدام مكتبة React Native.

أولًا سنلقي نظرة على خلفية المكتبة وتاريخها.

بداية التحريكات في React Native وتطوّرها

عند تشغيل برنامج عابر للمنصّات مكتوب بلغة جافاسكريبت تحتاج مكوّنات React Native على هاتفك إلى تبادل المعلومات بواسطة عنصر يسمّى الجسر (Bridge). التبادل عبر هذا العنصر غير متزامن (Asynchronous)، وهو ما يجعل التطبيقات المبنية على إطار العمل React Native تصبح بطيئة لأن الطلبات غير المتزامنة المارّة على الجسر تسدّ المسار أمام شفرة جافاسكريبت التي تتفاعل مع أجزاء إطار العمل.

للحصول على أداء عال يجب أن تصيّر (Render) التحريكات على خيط (Thread) واجهة المستخدم الأصلي في نظام التشغيل، وبما أن البيانات تحتاج للسَّلسَلة (Serialization) عبر الجسر فإن ذلك يؤدّي غالبًا إلى منع خيط جافاسكريبت، ممّا يتسبّب في نقص عدد الإطارات (Frame) على الشاشة. بقي هذا المشكل حاضرًا منذ 2015 حين كانت التحريكات تمثّل واحدة من العقبات الكبرى أمام إطار العمل React Native.

لحسن الحظ، تحسّنت الحالة منذ ذلك الوقت بفضل الدعم الكبير من أعضاء مجتمع المطورين، إذ أصبح من الشائع الآن إنجاز 60 إطارًا في الثانية الواحدة (Frame per second, FPS) في عمليات التحريك ضمن React Native. سهّلت واجهات تطبيقات برمجيّة تصريحيّة (Declarative API) مثل Animated عمليّة تنفيذ التحريكات التفاعلية.

استخدام واجهة التطبيقات البرمجية Animated لتحسين الأداء

يواجه المطورون حتى الآن العديد من القضايا المتعلقة بالأداء خاصة عندما يعملون على رسومات متحركة معقّدة.

كما ذكرنا سابقًا، تتسبب الاختناقات في الأداء التي تحدث عند تنفيذ الرسوم في React Native، بأعباء عمل كبيرة على خيوط جافاسكربت (JavaScript threads)، مما يقلّل من معدل إظهار الإطارات، وبالتالي يتسبّب في البطء عند استخدام البرنامج، وللتغلب على هذه المشكلة نحتاج إلى الحفاظ على معدل 60 إطارًا في الثانية الواحدة.

إن أفضل حل للحفاظ على معدّل الإطارات بالثانية هو استخدام Animated API لأنها تحسّن الزمن المطلوب للسَّلسَلة وعكسها (Serialization/Deserialization). تعمل Animated API عبر استخدام واجهة تطبيقات تصريحية لوصف التحريكات. الفكرة التي تقوم عليها هي التصريح بالتحريك كاملًا مرةً واحدة مقدّمًا لكي تُمكِن سَلسَلة التصريح في جافاسكريبت وإرسالها إلى الجسر. ينفّذ مشغّلٌ (Driver) التحريكاتِ إطارًا بعد الآخر.

تنفيد التحريكات ضمن React Native

توجد طرق عدة لتطبيق التحريك في React Native. في ما يلي الطرق التي أجد أن لديها فائدة أكبر.

القيم المتحرّكة Animated values

يتربّع تحريك القيم على قائمتي بوصفه لبنة أساسيّة للتحريكات في تطبيقات React. تشير هذه القيم عمومًا إلى قيم حقيقة تُحوَّل إلى أعداد حقيقية عند تمريرها مع مكوّن مُحرَّك.

فلننظر إلى المثال التالي.

Animated.timing(this.valueToAnimate, {
    toValue: 42;
    duration: 1000;
}).start()

نلاحظ في المثال السابق أننا أسندنا القيمة 42 إلى value.ToAnimate والتي ستنفّذ بعد 1000 ميلي ثانية.

كما يمكن التصريح عن قيم خاصيات التحريك الأخرى مثل الشفافية opacity أو الموضع position. في ما يلي مثال تطبيقي على الشفافية بقيم متحرّكة.

<Animated.View style={{ opacity: myAnimatedOpacity }} />  

مشغلات التحريك: Animated.timing و Animated.event و Animated.decay

انظر إلى المشغّلات على أنها عُقد في مخطّط بياني (Graph) تغيّر قيمةً متحرّكة مع كل إطار. على سبيل المثال، ستزداد القيمة المسندة إلى Animated.timing، بينما ستنقص القيمة المسندة إلى Animated.decay في كل إطار. لنلق نظرة على المثال التالي:

Animated.decay(this.valueToAnimate, {
   velocity: 2.0,
   deceleration: 0.9
}).start();

في هذا المثال، ستنفَّذ الرسوم المتحركة انطلاقًا من سرعة محددة بالقيمة 2.0، ثم تتناقص تدريجيًا خلال الوقت المحدَّد. أصبحت هذه الطريقة شائعة في التطبيقات العابرة للمنصّات مع بداية ظهور التوثيقات عن التصميم المسطّح (Material design). تبدو التحريكات بمظهر جيّد، وتوجد طرق عدّة لجعل التجربة لا تنسى.

يمكن كذلك استخدام Animated.event لتشغيل قيمة عند تمرير المستخدم:

<ScrollView onScroll={Animated.event(
  [{nativeEvent: {contentOffset: {y: this.state.scrollY}}}]
)}
>
</ScrollView>

في المثال السابق يُعيد Animated.event دالّة تعدّل قيمة الخاصيّة nativeEvent.content.offset.y في المكوّن scrollView لتأخد قيمة scrollY الحالية.

عمومًا، يمكن استخدام مشغلّات التحريك مع القيم المتحركة أو مع مشغّلات أخرى.

ملحوظة جانبية: انتبه إلى أنه عندما يحدّث المشغّل كل إطار فإن القيمة الجديدة تحدّث مباشرةً قيمة الخاصيّة View، وبالتالي يجب أن تكون حذرًا عند التصريح بالمتغيّرات وتنتبه إلى نطاقها أثناء استخدامها.

توابع التحويل Transform methods

تتيح لك توابع التحويل تحويل قيمة متحرّكة إلى قيمة متحرّكة جديدة. يمكنك استخدام توابع مثل Animated.add()‎ وAnimated.multiply()‎ وAnimated.interpolate()‎ لتنفيذ عمليّات تحويل بين القيم. يمكنك تنفيذ عمليّة تحويل على أي عقدة في المخطّط البياني قيد التحريك كالتالي:

newAnimated.Value(55).interpolate(.....) // Transformation operation using Animated.interpolate() method

تحريك الخاصيّات

الخاصيّات المتحرّكة هي عقد خاصّة تربط بين القيم المتحرّكة وخاصيّة في مكوّن. تُولَّد تلك الخاصيّات عند تصيير الخاصيّة Animated.view وإسناد خاصيّات لها.

فلننظر إلى الشفرة التالية:

Var opacity = new Animated.Value(0.7);
<Animated.View style={{ opacity }} />

أضفنا خاصيّة متحرّكة تُحوِّل القيمة 0.7 إلى خاصية. في حال حدّث التابع تلك القيمة، سينعكس التغيير على خاصية View.

تعمل التوابع الموصوفة أعلاه أساسًا بالتزامن مع الكائنات المتحرّكة (Animated objects) في React، كما أن توابع التحويل تؤدّي دورًا أساسيًّا في عمل تلك الكائنات.

يغيّر مشغّل التحريك (Animated.Timing أو Animated.Event أو Animated.Decay) قيم التحريك في كل إطار ثم تُمرر النتيجة عبر أي تابع من توابع التحويل لتُخزَّن بصيغة خاصية عرض (الشفافية Opacity أو قيمة التحويل).

يسلم مفسّر جافاسكريبت النتيجة إلى Native React حيث تُحدَّث الواجهة من خلال استدعاء التابع setNativeProps. في الأخير تُمرَّر الخاصيّة إلى iOS أو Android حيث يتسلم المكوّن UIView أو Android.View التحديثات.

تنفيذ الرسوم المتحركة باستخدام Animated API و Native Driver

تشرف مشغلات جافاسكريبت منذ ظهور Animated API على عملية تنفيذ التحريك، ولكن هذه الطريقة سببت تباطؤًا في عملية التحريك بسبب قلة عدد الإطارات المعروضة في الثانية ويعود السبب في ذلك أن العمل كله يتم في الخيط (Thread) الخاص بجافاسكريبت.

request_animation_frame.png

لتجاوز هذه المشكلة أضيفت مشغلات خاصة بإطار العمل React Native وبالتالي أصبحت قادرة على تنفيذ عملية التحريك كاملةً، إطارًا بعد الآخر، ضمن React Native.

عند استخدام المشغل الخاصة بإطار العمل React native مع Animated API فإن ذلك يسمح بتحديث العرض مباشرة دون الحاجة إلى إعادة حساب القيم مرة أخرى في جافاسكربت.

لاستخدام المشغّل الخاص بإطار العمل React يجب تعيين القيمة true إلى الخاصيّة useNativeDriver أثناء ضبط التحريكات.

useNativeDriver: true

استخدام PanResponder للتعامل مع لمس الشاشة في React Native

يمكن لواجهة التطبيقات Animated API تنفيذ غالبية الأعمال الروتينية عند عمل التحريكات في React Native، إلّا أنها محدودة في التعامل مع لمس الشاشة للتفاعل مع التحريكات، فهي غير قادرة على التجاوب مع الحركات التي تحدث خارج المكوّن ScrollView.

رغم أنه يمكن تنفيذ أشياء كثيرة عن طريق مكوّن ScrollView بسيط، إلّا أن التطبيقات على الأجهزة الجوّالة تبقى ناقصة بدون لمس الشاشة التي يستعملها المستخدم للتفاعل مع التحريكات عن طريق التمرير أو التدوير على سبيل المثال.

يمكن التعامل مع حركات اللمس في React Native بسلاسة باستخدام PanResponder مع Animated API.

يدمج PanResponder لمسات عدّة في حركة خاصّة، ويجعل لمسة واحدة تتجاوب مع لمسات إضافية لكي تعمل حركات اليد بسلاسة.

افتراضيًّا، يتمثّل PanResponder في مقبض InteractionManager يمنع الأحداث العاملة على خيط جافاسكريبت من تعطيل حركات اللمس.

تحسين زمن التشغيل لانتقالات التصفح البطيئة

عادةً يحتاج أي تحريك يستدعي الانتقال من شاشة إلى أخرى في React Native إلى استخدام مكوّنات التصفّح. تُستخدَم مكوّنات التصفّح مثل React Navigation عادةً للانتقالات أثناء التصفح.

تحدث الانتقالات أثناء التصفّح في React Native عادةً في خيوط جافاسكريبت، ممّا قد يتسبّب في انتقالات بطيئة في الأجهزة الرخيصة أو ذات الذاكرة المحدودة (أجهزة أندرويد أساسًا إذ أن الأجهزة العاملة بنظام iOS تتعامل مع الأمر بفاعلية أكبر). تحدث انتقالات التصفح البطيئة عادة عندما يحاول React Native تصيير شاشة جديدة في حين أنه ما زالت تُنفَّذ تحريكات في الخلفية.

لتفادي حالات مثل تلك المذكورة أعلاه، يسمح مدير التفاعلات (InteractionManager) بجدولة الأنشطة التي تأخذ وقتًا في التنفيذ بجدولتها بعد تنفيذ تحريك أو تفاعل في خيط جافاسكريبت.

تحريك التخطيطات Layouts

تعمل واجهة التطبيقات البرمجية LayoutAnimation على تحريك الشاشة تلقائيًّا إلى الوضعية الموالية عندما يحدث التخطيط (Layout) الموالي. تُنفَّذ واجهة التطبيقات تلك ضمن خيط واجهة المستخدم (UI Thread) وهو ما يجعلها ذات أداء عال.

تُطبَّق التحريكات المعدَّة عن طريق LayoutAnimation، فور استدعائها، على كل المكوّنات؛ بخلاف واجهة Animated التي يمكنك عن طريقها التحكّم في القيم المخصوصة بتحريكها. يمكن لواجهة LayoutAnimation تحريك كل شيء في التصيير القادم، لذا يجب استدعاؤها قبل استدعاء setState.

يضمن إعداد تحريك للتخطيط قبل استدعاء setState تحريكات سلسة في خيط Native ويحول دون تأثر التحريكات بتنفيذ شفرة برمجية عند إطلاق دور setState أخرى (وهو ما يتسبّب - في الظروف العادية - في تهديد تحريكات تطبيقك).

توجد طريقة أخرى لاستخدام LayoutAnimation وتتمثّل في استدعائها داخل التابع WillReceiveProps. استدع بكل بساطة التابع LayoutAnimation.configureNext مع تمرير الوسائط المناسبة لإعداد التحريك، كما هو موضَّح أدناه:

LayoutAnimation.configureNext(animationConfiguration, callbackCompletionMethod); 
this.setState({ stateToChange: newStateValue });

لا تدعم واجهة LayoutAnimation سوى خاصيّتين هما الشفافية (Opacity) وقابلية التكيّف (Scalability).

تتعرّف الواجهة على العروض (Views) باستخدام مفاتيحها الفريدة وحساب وضعيّتها المتوقّعة. علاوة على ذلك، تحرّك تغيّرات الإطارات ما دام العرض يحتفظ بالمفتاح نفسه بين تغيّر الحالات.

تعمل التحريكات المطبَّقة بواسطة LayoutAnimation ضمن الخيط الأصلي لواجهة المستخدم في نظام التشغيل، وهو أمر جيّد من ناحية الأداء، إلّا أنه يمثّل تحديًّا إذا دعت الحاجة لتحريك كل الخاصيّات بين حالات الإطارات.

خاتمة

لا يدخل هذا المقال في تفاصيل التحريكات في React Native ويكتفي بأمور سطحية، إلّا أن هناك قاعدة أساسية عند التعامل مع إطار العمل React Native وهي استخدام واجهة التطبيقات البرمجية Animated API ما دام ذلك ممكنا. بالنسبة لحركات اللمس، استخدم PanResponder بجانب Animated API.

يمكن التغلّب على الكثير من المشاكل التي تواجهها بالاستفادة من مشغّل Native driver إلى جانب واجهة Animated. إنْ استمرّت مشاكل الأداء رغم ذلك فالزم LayoutAnimation.

ترجمة - وبتصرّف - للمقال Getting started with React Native animations لصاحبه Rakshit Soral.





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن