الفرق بين المراجعتين لصفحة: «ReactNative/animations»
أنشأ الصفحة ب'<noinclude>{{DISPLAYTITLE:التحريكات في React Native}}</noinclude> التحريكات (Animations) مهمة جدًا لتجربة مستخدمٍ رائعة. ي...' |
جميل-بيلوني (نقاش | مساهمات) طلا ملخص تعديل |
||
(6 مراجعات متوسطة بواسطة 4 مستخدمين غير معروضة) | |||
سطر 4: | سطر 4: | ||
يوفر React Native نظامي تحريك يُكمِّلان بعضهما البعض: | يوفر React Native نظامي تحريك يُكمِّلان بعضهما البعض: | ||
* واجهة Animated للتحكم الحُبَيْبِيّ (granular) والتفاعليّ بقيمٍ محددة. | * واجهة <code>Animated</code> للتحكم الحُبَيْبِيّ (granular) والتفاعليّ بقيمٍ محددة. | ||
* واجهة LayoutAnimation للتعامل مع تحريك التخطيط العام (global layout). | * واجهة <code>LayoutAnimation</code> للتعامل مع تحريك التخطيط العام (global layout). | ||
==واجهة <code>Animated</code> | ==واجهة <code>Animated</code> البرمجية== | ||
صُمِّمَت واجهة <code>Animated</code> البرمجيّة لتسهيل التعبير عن مجموعة متنوعة من أنماط التحريك والتفاعل بطريقة فائقة الأداء. تركّز واجهة <code>Animated</code> على العلاقات التعريفية بين المدخلات (inputs) والمخرجات (outputs)، مع وجود تحويلات قابلة للضّبط بينهما، إضافة إلى تابعي <code>start</code> و<code>stop</code> بسيطين للتحكم في تنفيذ التّحريك على أساس الوقت. | صُمِّمَت واجهة <nowiki/>[[ReactNative/animated|<code>Animated</code>]] البرمجيّة لتسهيل التعبير عن مجموعة متنوعة من أنماط التحريك والتفاعل بطريقة فائقة الأداء. تركّز واجهة <code>Animated</code> على العلاقات التعريفية بين المدخلات (inputs) والمخرجات (outputs)، مع وجود تحويلات قابلة للضّبط بينهما، إضافة إلى تابعي <code>start</code> و<code>stop</code> بسيطين للتحكم في تنفيذ التّحريك على أساس الوقت. | ||
تُصدّر واجهة <code>Animated</code> أربعة أنواع من المكونات القابلة للتّحريك: <code>View</code>، و<code>Text</code>، و<code>Image</code>، و<code>ScrollView</code>، ويمكنك كذلك إنشاء مُكوّنات خاصّة بك باستخدام التّابع <code>Animated.createAnimatedComponent()</code>. | تُصدّر واجهة <code>Animated</code> أربعة أنواع من المكونات القابلة للتّحريك: <code>View</code>، و<code>Text</code>، و<code>Image</code>، و<code>ScrollView</code>، ويمكنك كذلك إنشاء مُكوّنات خاصّة بك باستخدام التّابع <code>Animated.createAnimatedComponent()</code>. | ||
إليك مثال فيه تبْزُغُ (fades in) واجهة حاوية ببطء عندما تصيَّر أي تضاف إلى الشاشة ([https://snack.expo.dev/@hsoubwiki/example-animated تجربة حية]): | |||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
import React from 'react'; | import React, { useRef, useEffect } from 'react'; | ||
import { Animated, Text, View } from 'react-native'; | import { Animated, Text, View } from 'react-native'; | ||
const FadeInView = (props) => { | |||
const fadeAnim = useRef(new Animated.Value(0)).current // القيمة الابتدائية للعتامة opacity: 0 | |||
React.useEffect(() => { | |||
Animated.timing( | |||
fadeAnim, | |||
Animated.timing( | |||
{ | { | ||
toValue: 1, | toValue: 1, | ||
duration: 10000, | duration: 10000, | ||
} | } | ||
).start(); | ).start(); | ||
} | }, [fadeAnim]) | ||
return ( | |||
<Animated.View // عرضٌ مُحرَّكٌ خاصّ | |||
style={{ | |||
...props.style, | |||
opacity: fadeAnim, // ربط العتامَة بالقيمة المُحرَّكة | |||
}} | |||
> | |||
{props.children} | |||
</Animated.View> | |||
); | |||
} | } | ||
سطر 53: | سطر 46: | ||
// `View` | // `View` | ||
// في مُكوّناتك | // في مُكوّناتك | ||
export default | |||
export default () => { | |||
return ( | |||
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}> | |||
<FadeInView style={{width: 250, height: 50, backgroundColor: 'powderblue'}}> | |||
<Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>Fading in</Text> | |||
</FadeInView> | |||
</View> | |||
) | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
لننظر في تفاصيل الشيفرة أعلاه. في الدّالة البانية (constructor) للمكوّن | لننظر في تفاصيل الشيفرة أعلاه. في الدّالة البانية (constructor) للمكوّن <code>FadeInView</code>، نُهيّئُ قيمةَ <code>Animated.Value</code> جديدةً تُمثِّل قيمةً مُتحرِّكةً ونُسمِّيها <code>fadeAnim</code> كجزء من الحالة (<code>state</code>). تُربَط خاصيةُ عتامةٍ (opacity) على الواجهة <code>View</code> مع هذه القيمة المتحركة. وراء الكواليس، تُستَخرَج القيمة الرقمية وتُستخدَم لضبط العتامة. | ||
عندما يوصَل المُكوّن تُضبَطُ العتامة إلى 0. وبعد ذلك | عندما يوصَل المُكوّن تُضبَطُ العتامة إلى 0. وبعد ذلك تبدأ حركة متسارعة (easing animation) تطبَّق على القيمة المتحركة <code>fadeAnim</code>، ما سيُحدِّث جميع القيم المربوطة التابعة لها (في هذه الحالة، هناك قيمة مربوطةٌ واحدةٌ فقط تُمثِّل العتامة) على كل إطارٍ (frame) أثناء نقل القيمة إلى القيمة النهائية 1. | ||
يتم هذا بطريقة محسّنةٍ أسرعَ من استدعاءِ setState لتغيير الحالة وإعادة التصيير. | يتم هذا بطريقة محسّنةٍ أسرعَ من استدعاءِ <code>setState</code> لتغيير الحالة وإعادة التصيير. ولأنّ الضبطَ تعريفيٌّ (declarative configuration) بأكمله، فسنكون قادرين على إجراء مزيد من التحسينات التي تقوم بسَلْسَلَةِ (serialize) الضّبط وتشغيل التحريك على سِلسِلَةٍ ذات أولوية عالية (high-priority thread). | ||
ولأنّ الضبطَ تعريفيٌّ (declarative configuration) بأكمله، فسنكون قادرين على إجراء مزيد من التحسينات التي تقوم بسَلْسَلَةِ (serialize) الضّبط وتشغيل التحريك على سِلسِلَةٍ ذات أولوية عالية (high-priority thread). | |||
===ضبط التحريكات=== | ===ضبط التحريكات=== | ||
يُمكن ضبط التحريكات بعدّة معاملاتٍ وخيارات. إذ يمكن تعديل دوال | يُمكن ضبط التحريكات بعدّة معاملاتٍ وخيارات. إذ يمكن تعديل دوال التسارع (easing functions) مخصّصة ومحددة مسبقًا، وتعديل التأخير (delay)، والمُدَدِ (durations)، وعوامل الِاضْمِحْلَالِ (decay factors)، وثوابت الوَثْبِ (spring constants)، وغير ذلك حسب نوع التّحريك. | ||
تُوفّر واجهة Animated العديد من أنواع التّحريك، وأكثرها استخدامًا هو التحريكُ <code>Animated.timing()</code>، الذي يدعم تحريك قيمةٍ بمرور الوقت باستخدام إحدى دوال التّخفيف المُعرَّفة مُسبقًا (predefined)، أو يمكنك استخدام دوال خاصّة بك. عادةً ما تُستخدَم دوال التّخفيف في التّحريك لإظهار تسارعٍ تدريجيّ للأشياء ولإبطائها. | تُوفّر واجهة <code>Animated</code> العديد من أنواع التّحريك، وأكثرها استخدامًا هو التحريكُ <code>[[ReactNative/animated#timing|Animated.timing()]]</code>، الذي يدعم تحريك قيمةٍ بمرور الوقت باستخدام إحدى دوال التّخفيف المُعرَّفة مُسبقًا (predefined)، أو يمكنك استخدام دوال خاصّة بك. عادةً ما تُستخدَم دوال التّخفيف في التّحريك لإظهار تسارعٍ تدريجيّ للأشياء ولإبطائها. | ||
افتراضيًّا، سيستخدم التّابعُ <code>timing</code> منحنى ظهورٍ مُخفَّف واختفاء مُخفَّف | افتراضيًّا، سيستخدم التّابعُ <code>timing</code> منحنى ظهورٍ مُخفَّف واختفاء مُخفَّف easeInOut، ما يُظهر تسارعًا تدريجيًّا إلى السرعة الكاملة ويختتم بتباطؤٍ تدريجيّ إلى التوقف. يمكنك تحديد دالّة تسارع مختلفة عبر تمريرها إلى المُعامل <code>easing</code>. يُمكنك كذلك تحديد مدة <code>duration</code> مخصّصةٍ، أو تأخيرٍ <code>delay</code> قبل بدء التّحريك. | ||
على سبيل المثال، يُمكننا إنشاء تحريك يدوم لثانيّتين لكائنٍ يرتدّ (backs up) ارتدادًا طفيفًا قبل الانتقال إلى موضعه النهائي كما يلي: | على سبيل المثال، يُمكننا إنشاء تحريك يدوم لثانيّتين لكائنٍ يرتدّ (backs up) ارتدادًا طفيفًا قبل الانتقال إلى موضعه النهائي كما يلي: | ||
سطر 85: | سطر 76: | ||
toValue: 100, | toValue: 100, | ||
easing: Easing.back(), | easing: Easing.back(), | ||
duration: 2000 | duration: 2000 | ||
}).start(); | }).start(); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
انظر قسم ضبط التحريكات في توثيق واجهة <code>Animated</code> للاستزادة حول جميع خيارات الضّبط المدعومة. | انظر [[ReactNative/animated|قسم ضبط التحريكات في توثيق واجهة <code>Animated</code>]] للاستزادة حول جميع خيارات الضّبط المدعومة. | ||
===تركيب التحريكات=== | ===تركيب التحريكات=== | ||
يمكن دمج التحريكات وتشغيلها بالتسلسل أو بالتوازي. يمكن تشغيل التحريكات التسلسلية فور انتهاء التحريك الذي يسبقها، أو يمكن تشغيلها بعد مدّة تأخير محددة. توفر واجهة Animated عدة توابع لهذا، مثل <code>sequence()</code>، و<code>delay()</code>، كل تابعٍ يأخذ ببساطةٍ مصفوفةً من التحريكات المرغوب تشغيلها، ويستدعي تلقائيًّا <code>start()</code>، أو <code>stop()</code> حسب الحاجة. | يمكن دمج التحريكات وتشغيلها بالتسلسل أو بالتوازي. يمكن تشغيل التحريكات التسلسلية فور انتهاء التحريك الذي يسبقها، أو يمكن تشغيلها بعد مدّة تأخير محددة. توفر واجهة <code>Animated</code> عدة توابع لهذا، مثل <code>sequence()</code>، و<code>delay()</code>، كل تابعٍ يأخذ ببساطةٍ مصفوفةً من التحريكات المرغوب تشغيلها، ويستدعي تلقائيًّا <code>start()</code>، أو <code>stop()</code> حسب الحاجة. | ||
على سبيل المثال، يهبط (coasts) التحريك التالي تدريجيًّا إلى نقطة توقّف، ثم يرتدّ واثبًا (springs back) أثناء الالتفاف (twirling) بالتوازي: | على سبيل المثال، يهبط (coasts) التحريك التالي تدريجيًّا إلى نقطة توقّف، ثم يرتدّ واثبًا (springs back) أثناء الالتفاف (twirling) بالتوازي: | ||
سطر 99: | سطر 90: | ||
Animated.decay(position, { | Animated.decay(position, { | ||
// اهبِط إلى نُقطَة توقّف | // اهبِط إلى نُقطَة توقّف | ||
velocity: {x: gestureState.vx, y: gestureState.vy}, // | velocity: { x: gestureState.vx, y: gestureState.vy }, // velocity from gesture release | ||
deceleration: 0.997 | deceleration: 0.997 | ||
}), | }), | ||
Animated.parallel([ | Animated.parallel([ | ||
// بعد التّلاشي (بالتوازي) | // بعد التّلاشي (بالتوازي) | ||
Animated.spring(position, { | Animated.spring(position, { | ||
toValue: {x: 0, y: 0} | toValue: { x: 0, y: 0 } // return to start | ||
}), | }), | ||
Animated.timing(twirl, { | Animated.timing(twirl, { | ||
// | // and twirl | ||
toValue: 360 | toValue: 360 | ||
}) | }) | ||
]) | ]) | ||
]).start(); // ابدأ سلسلة التحريكات | ]).start(); // ابدأ سلسلة التحريكات | ||
</syntaxhighlight> | </syntaxhighlight> | ||
إذا أُوقِفَ تحريكٌ أو قُوطِع، فستُوقَف جميع التحريكات الأخرى في المجموعة كذلك. يحتوي التّابع <code>Animated.parallel</code> خيارًا باسم <code>stopTogether</code> يمكن ضبطُ قيمتِه إلى false لتعطيل هذا السلوك. | إذا أُوقِفَ تحريكٌ أو قُوطِع، فستُوقَف جميع التحريكات الأخرى في المجموعة كذلك. يحتوي التّابع <code>Animated.parallel</code> خيارًا باسم <code>stopTogether</code> يمكن ضبطُ قيمتِه إلى <code>false</code> لتعطيل هذا السلوك. | ||
لقائمةٍ كاملة بأساليب التركيب، انظر قسم تركيب التّحريكات في توثيق واجهة <code>Animated</code> البرمجيّة. | لقائمةٍ كاملة بأساليب التركيب، انظر [[ReactNative/animated|قسم تركيب التّحريكات في توثيق واجهة <code>Animated</code> البرمجيّة.]] | ||
===دمج القيم المتحركة=== | ===دمج القيم المتحركة=== | ||
سطر 127: | سطر 118: | ||
Animated.spring(a, { | Animated.spring(a, { | ||
toValue: 2 | toValue: 2 | ||
}).start(); | }).start(); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===الاستيفاء (Interpolation)=== | ===الاستيفاء (Interpolation)=== | ||
يمكن تمرير كل خاصية عبر استيفاءٍ أولاً. يربط الاستيفاء مجالات المدخلات (input ranges) مع مجالات المخرجات (output ranges)، وعادة ما يستخدم استيفاءٌ خطّي، ولكنّ هذه الطريقة تدعم كذلك دوال | يمكن تمرير كل خاصية عبر استيفاءٍ أولاً. يربط الاستيفاء مجالات المدخلات (input ranges) مع مجالات المخرجات (output ranges)، وعادة ما يستخدم استيفاءٌ خطّي، ولكنّ هذه الطريقة تدعم كذلك دوال التسارع. افتراضيًّا، سيُستكمَل المنحنى استقرائيًّا (extrapolate) خارج المجالات المعطاة، لكن يمكنك شدّ (clamp) قيمة المُخرج كذلك كي لا تخرج عن حدود المجالات المُعطاة. | ||
هذا مثال على ربطٍ بسيط لتحويل المجال 0-1 إلى المجال 0-100: | هذا مثال على ربطٍ بسيط لتحويل المجال 0-1 إلى المجال 0-100: | ||
سطر 137: | سطر 128: | ||
value.interpolate({ | value.interpolate({ | ||
inputRange: [0, 1], | inputRange: [0, 1], | ||
outputRange: [0, 100] | outputRange: [0, 100] | ||
}); | }); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
على سبيل المثال، قد ترغب بنقل قيمة التحريك Animated.Value الخاصة بك من 0 إلى 1، مع تحريك الموضع من 150px إلى 0px و العتامة من 0 إلى 1. يمكن القيام بذلك بسهولة عن طريق تعديل النمط <code>style</code> من المثال أعلاه هكذا: | على سبيل المثال، قد ترغب بنقل قيمة التحريك <code>Animated.Value</code> الخاصة بك من 0 إلى 1، مع تحريك الموضع من <code>150px</code> إلى <code>0px</code> و العتامة من 0 إلى 1. يمكن القيام بذلك بسهولة عن طريق تعديل النمط <code>style</code> من المثال أعلاه هكذا: | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
style={{ | |||
opacity: this.state.fadeAnim, // ربط القيمة المتحرّكة مُباشرةً | opacity: this.state.fadeAnim, // ربط القيمة المتحرّكة مُباشرةً | ||
transform: [{ | transform: [{ | ||
سطر 154: | سطر 145: | ||
يدعم التّابع <code>interpolate()</code> كذلك شرائح مجالاتٍ متعددة، ما يُسهل تحديد المناطق الهامدة (dead zones) وحيلٍ أخرى. | يدعم التّابع <code>interpolate()</code> كذلك شرائح مجالاتٍ متعددة، ما يُسهل تحديد المناطق الهامدة (dead zones) وحيلٍ أخرى. | ||
على سبيل المثال، للحصول على علاقة نفيٍ (negation relationship) عند النّقطة | على سبيل المثال، للحصول على علاقة نفيٍ (negation relationship) عند النّقطة -300 تنتقِل إلى 0 عند النّقطة -100، ثم تنتقِل إلى 1 عند النّقطة 0، ثم تعود مُجدّدًا إلى الصفر عند النّقطة 100 ثمّ تُـتبَع بمنطقة هامدة تبقى عند النّقطة 0 لما وراء ذلك، فستكون الشيفرة مشابهة لما يلي: | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
value.interpolate({ | value.interpolate({ | ||
inputRange: [-300, -100, 0, 100, 101], | inputRange: [-300, -100, 0, 100, 101], | ||
outputRange: [300, 0, 1, 0, 0] | outputRange: [300, 0, 1, 0, 0] | ||
}); | }); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
سطر 181: | سطر 172: | ||
value.interpolate({ | value.interpolate({ | ||
inputRange: [0, 360], | inputRange: [0, 360], | ||
outputRange: ['0deg', '360deg'] | outputRange: ['0deg', '360deg'] | ||
}); | }); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
يدعم التّابع <code>interpolate()</code> كذلك دوال تخفيف محدّدة، والتي يوجد عدد منها في وحدة <code>Easing</code>. إضافةً إلى أنّ للتّابعِ <code>interpolate()</code> سلوكًا قابلًا للضّبط لاستقراء (extrapolation) المجال المُخرَج <code>outputRange</code>. يمكنك ضبط الاستقراء عبر ضبط خيارات <code>extrapolate</code>، و<code>extrapolateLeft</code>، و<code>extrapolateRight</code>. القيمة الافتراضية هي <code>extend</code> للتوسيع، لكن يمكنك استخدام <code>clamp</code> لمنع القيمة المُخرجَة من تجاوز مجال <code>outputRange</code>. | يدعم التّابع <code>interpolate()</code> كذلك دوال تسارع/تخفيف محدّدة، والتي يوجد عدد منها في وحدة [[ReactNative/easing|<code>Easing</code>]]. إضافةً إلى أنّ للتّابعِ <code>interpolate()</code> سلوكًا قابلًا للضّبط لاستقراء (extrapolation) المجال المُخرَج <code>outputRange</code>. يمكنك ضبط الاستقراء عبر ضبط خيارات <code>extrapolate</code>، و<code>extrapolateLeft</code>، و<code>extrapolateRight</code>. القيمة الافتراضية هي <code>extend</code> للتوسيع، لكن يمكنك استخدام <code>clamp</code> لمنع القيمة المُخرجَة من تجاوز مجال <code>outputRange</code>. | ||
===تتبع القيم الديناميكية=== | ===تتبع القيم الديناميكية=== | ||
يمكن للقيم المتحركة تَتبّعُ القيم الأخرى كذلك. ما عليك سوى تعيين قيمةِ <code>toValue</code> خاصّةٍ بتحريكٍ إلى قيمة متحرّكة أخرى بدلاً من عددٍ عادي. على سبيل المثال، يمكن إنجاز تحريك "Chat Heads" كذلك المستخدم في تطبيق Messenger على Android عبر تثبيت <code>spring()</code>على قيمة متحركة أخرى، أو باستعمال <code>timing()</code> ومدّة <code>duration</code> تُساوي 0 للتَتبّعٍ مُشدَّد. ويمكن كذلك إنجاز التّحريك عبر تركيب التحريكات بالاستيفاء: | يمكن للقيم المتحركة تَتبّعُ القيم الأخرى كذلك. ما عليك سوى تعيين قيمةِ <code>toValue</code> خاصّةٍ بتحريكٍ إلى قيمة متحرّكة أخرى بدلاً من عددٍ عادي. على سبيل المثال، يمكن إنجاز تحريك "Chat Heads" كذلك المستخدم في تطبيق Messenger على Android عبر تثبيت <code>spring()</code>على قيمة متحركة أخرى، أو باستعمال <code>timing()</code> ومدّة <code>duration</code> تُساوي 0 للتَتبّعٍ مُشدَّد. ويمكن كذلك إنجاز التّحريك عبر تركيب التحريكات بالاستيفاء: | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
Animated.spring(follower, {toValue: leader}).start(); | Animated.spring(follower, { toValue: leader }).start(); | ||
Animated.timing(opacity, { | Animated.timing(opacity, { | ||
toValue: pan.x.interpolate({ | toValue: pan.x.interpolate({ | ||
inputRange: [0, 300], | inputRange: [0, 300], | ||
outputRange: [1, 0] | outputRange: [1, 0] | ||
}) | }) | ||
}).start(); | }).start(); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
سطر 201: | سطر 192: | ||
===تتبع الإيماءات=== | ===تتبع الإيماءات=== | ||
يمكن ربط الإيماءات (Gestures) مثل المسح (panning) أو التمرير (scrolling)، وأحداثٍ (events) أخرى مباشرةً بالقيم المتحركة باستخدام Animated.event. يتم ذلك باستخدام بنية ربطٍ منظمة بحيث يمكن استخراج القيم من كائنات الأحداث (event objects) المعقدة. المستوى الأول هو مصفوفة للسماح بالرّبط عبر عدة مُعاملات، وتحتوي هذه المصفوفة على كائنات متداخلة. | يمكن ربط الإيماءات (Gestures) مثل المسح (panning) أو التمرير (scrolling)، وأحداثٍ (events) أخرى مباشرةً بالقيم المتحركة باستخدام <code>Animated.event</code>. يتم ذلك باستخدام بنية ربطٍ منظمة بحيث يمكن استخراج القيم من كائنات الأحداث (event objects) المعقدة. المستوى الأول هو مصفوفة للسماح بالرّبط عبر عدة مُعاملات، وتحتوي هذه المصفوفة على كائنات متداخلة. | ||
على سبيل المثال، عند التّعامل مع إيماءات التمرير الأفقي (horizontal scrolling)، يمكنك القيام بما يلي لربط <code>event.nativeEvent.contentOffset.x</code> بقيمةٍ مُتحرِّكة (تُهيّئ بالدالة <code>Animated.Value</code>) باسم <code>scrollX</code>: | على سبيل المثال، عند التّعامل مع إيماءات التمرير الأفقي (horizontal scrolling)، يمكنك القيام بما يلي لربط <code>event.nativeEvent.contentOffset.x</code> بقيمةٍ مُتحرِّكة (تُهيّئ بالدالة <code>Animated.Value</code>) باسم <code>scrollX</code>: | ||
سطر 215: | سطر 206: | ||
)} | )} | ||
</syntaxhighlight> | </syntaxhighlight> | ||
عند استخدام واجهة | ينفّذ المثال التالي سير تمرير أفقي (horizontal scrolling carousel) حيث تتحرك مؤشرات موضع التمرير باستخدام <code>Animated.event</code> المستخدم في واجهة التمرير <code>ScrollView</code> | ||
=== مثال واجهة تمرير مع حدث التحريك === | |||
* [https://snack.expo.dev/@hsoubwiki/scrollview-with-animated-function-component مثال لمكون الدالة (Function component)] | |||
<syntaxhighlight lang="javascript"> | |||
import React, { useRef } from "react"; | |||
import { | |||
SafeAreaView, | |||
ScrollView, | |||
Text, | |||
StyleSheet, | |||
View, | |||
ImageBackground, | |||
Animated, | |||
useWindowDimensions | |||
} from "react-native"; | |||
const images = new Array(6).fill('https://images.unsplash.com/photo-1556740749-887f6717d7e4'); | |||
const App = () => { | |||
const scrollX = useRef(new Animated.Value(0)).current; | |||
const { width: windowWidth } = useWindowDimensions(); | |||
return ( | |||
<SafeAreaView style={styles.container}> | |||
<View style={styles.scrollContainer}> | |||
<ScrollView | |||
horizontal={true} | |||
style={styles.scrollViewStyle} | |||
pagingEnabled | |||
showsHorizontalScrollIndicator={false} | |||
onScroll={Animated.event([ | |||
{ | |||
nativeEvent: { | |||
contentOffset: { | |||
x: scrollX | |||
} | |||
} | |||
} | |||
])} | |||
scrollEventThrottle={1} | |||
> | |||
{images.map((image, imageIndex) => { | |||
return ( | |||
<View | |||
style={{ width: windowWidth, height: 250 }} | |||
key={imageIndex} | |||
> | |||
<ImageBackground source={{ uri: image }} style={styles.card}> | |||
<View style={styles.textContainer}> | |||
<Text style={styles.infoText}> | |||
{"Image - " + imageIndex} | |||
</Text> | |||
</View> | |||
</ImageBackground> | |||
</View> | |||
); | |||
})} | |||
</ScrollView> | |||
<View style={styles.indicatorContainer}> | |||
{images.map((image, imageIndex) => { | |||
const width = scrollX.interpolate({ | |||
inputRange: [ | |||
windowWidth * (imageIndex - 1), | |||
windowWidth * imageIndex, | |||
windowWidth * (imageIndex + 1) | |||
], | |||
outputRange: [8, 16, 8], | |||
extrapolate: "clamp" | |||
}); | |||
return ( | |||
<Animated.View | |||
key={imageIndex} | |||
style={[styles.normalDot, { width }]} | |||
/> | |||
); | |||
})} | |||
</View> | |||
</View> | |||
</SafeAreaView> | |||
); | |||
} | |||
const styles = StyleSheet.create({ | |||
container: { | |||
flex: 1, | |||
alignItems: "center", | |||
justifyContent: "center" | |||
}, | |||
scrollContainer: { | |||
height: 300, | |||
alignItems: "center", | |||
justifyContent: "center" | |||
}, | |||
card: { | |||
flex: 1, | |||
marginVertical: 4, | |||
marginHorizontal: 16, | |||
borderRadius: 5, | |||
overflow: "hidden", | |||
alignItems: "center", | |||
justifyContent: "center" | |||
}, | |||
textContainer: { | |||
backgroundColor: "rgba(0,0,0, 0.7)", | |||
paddingHorizontal: 24, | |||
paddingVertical: 8, | |||
borderRadius: 5 | |||
}, | |||
infoText: { | |||
color: "white", | |||
fontSize: 16, | |||
fontWeight: "bold" | |||
}, | |||
normalDot: { | |||
height: 8, | |||
width: 8, | |||
borderRadius: 4, | |||
backgroundColor: "silver", | |||
marginHorizontal: 4 | |||
}, | |||
indicatorContainer: { | |||
flexDirection: "row", | |||
alignItems: "center", | |||
justifyContent: "center" | |||
} | |||
}); | |||
export default App; | |||
</syntaxhighlight> | |||
* [https://snack.expo.dev/@hsoubwiki/scrollview-with-animated-class-component مثال لمكون الصنف (class component)] | |||
<syntaxhighlight lang="javascript"> | |||
import React, { Component } from "react"; | |||
import { | |||
SafeAreaView, | |||
ScrollView, | |||
Text, | |||
StyleSheet, | |||
View, | |||
ImageBackground, | |||
Animated, | |||
Dimensions | |||
} from "react-native"; | |||
const images = new Array(6).fill('https://images.unsplash.com/photo-1556740749-887f6717d7e4'); | |||
const window = Dimensions.get("window"); | |||
export default class App extends Component { | |||
scrollX = new Animated.Value(0); | |||
state = { | |||
dimensions: { | |||
window | |||
} | |||
}; | |||
onDimensionsChange = ({ window }) => { | |||
this.setState({ dimensions: { window } }); | |||
}; | |||
componentDidMount() { | |||
Dimensions.addEventListener("change", this.onDimensionsChange); | |||
} | |||
componentWillUnmount() { | |||
Dimensions.removeEventListener("change", this.onDimensionsChange); | |||
} | |||
render() { | |||
const windowWidth = this.state.dimensions.window.width; | |||
return ( | |||
<SafeAreaView style={styles.container}> | |||
<View style={styles.scrollContainer}> | |||
<ScrollView | |||
horizontal={true} | |||
style={styles.scrollViewStyle} | |||
pagingEnabled | |||
showsHorizontalScrollIndicator={false} | |||
onScroll={Animated.event([ | |||
{ | |||
nativeEvent: { | |||
contentOffset: { | |||
x: this.scrollX | |||
} | |||
} | |||
} | |||
])} | |||
scrollEventThrottle={1} | |||
> | |||
{images.map((image, imageIndex) => { | |||
return ( | |||
<View | |||
style={{ | |||
width: windowWidth, | |||
height: 250 | |||
}} | |||
key={imageIndex} | |||
> | |||
<ImageBackground source={{ uri: image }} style={styles.card}> | |||
<View style={styles.textContainer}> | |||
<Text style={styles.infoText}> | |||
{"Image - " + imageIndex} | |||
</Text> | |||
</View> | |||
</ImageBackground> | |||
</View> | |||
); | |||
})} | |||
</ScrollView> | |||
<View style={styles.indicatorContainer}> | |||
{images.map((image, imageIndex) => { | |||
const width = this.scrollX.interpolate({ | |||
inputRange: [ | |||
windowWidth * (imageIndex - 1), | |||
windowWidth * imageIndex, | |||
windowWidth * (imageIndex + 1) | |||
], | |||
outputRange: [8, 16, 8], | |||
extrapolate: "clamp" | |||
}); | |||
return ( | |||
<Animated.View | |||
key={imageIndex} | |||
style={[styles.normalDot, { width }]} | |||
/> | |||
); | |||
})} | |||
</View> | |||
</View> | |||
</SafeAreaView> | |||
); | |||
} | |||
} | |||
const styles = StyleSheet.create({ | |||
container: { | |||
flex: 1, | |||
alignItems: "center", | |||
justifyContent: "center" | |||
}, | |||
scrollContainer: { | |||
height: 300, | |||
alignItems: "center", | |||
justifyContent: "center" | |||
}, | |||
card: { | |||
flex: 1, | |||
marginVertical: 4, | |||
marginHorizontal: 16, | |||
borderRadius: 5, | |||
overflow: "hidden", | |||
alignItems: "center", | |||
justifyContent: "center" | |||
}, | |||
textContainer: { | |||
backgroundColor: "rgba(0,0,0, 0.7)", | |||
paddingHorizontal: 24, | |||
paddingVertical: 8, | |||
borderRadius: 5 | |||
}, | |||
infoText: { | |||
color: "white", | |||
fontSize: 16, | |||
fontWeight: "bold" | |||
}, | |||
normalDot: { | |||
height: 8, | |||
width: 8, | |||
borderRadius: 4, | |||
backgroundColor: "silver", | |||
marginHorizontal: 4 | |||
}, | |||
indicatorContainer: { | |||
flexDirection: "row", | |||
alignItems: "center", | |||
justifyContent: "center" | |||
} | |||
}); | |||
</syntaxhighlight>عند استخدام واجهة <code>[[ReactNative/panresponder|PanResponder]]</code>، يمكنك استخدام الشيفرة التالية لاستخراج مواضع x و y من <code>gestureState.dx</code> و <code>gestureState.dy</code>. نستخدم القيمة <code>null</code> في الموضع الأول للمصفوفة، لأنّنا لا نحتاج إلّا إلى المتغير الثاني الذي يُمرَّر إلى معالج <code>PanResponder</code>، والذي يُمثِّل حالة الإيماءة <code>gestureState</code>. | |||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
onPanResponderMove={Animated.event( | onPanResponderMove={Animated.event( | ||
[null, // تجاهل الحدث الأصيل | [null, // تجاهل الحدث الأصيل | ||
// dx | // استخرج dx و dy من gestureState | ||
// 'pan.x = gestureState.dx, pan.y = gestureState.dy'مثل | |||
// | |||
{dx: pan.x, dy: pan.y} | {dx: pan.x, dy: pan.y} | ||
])} | ])} | ||
</syntaxhighlight>إليك مثال لواجهة <code>PanResponder</code> مع حدث التحريك: | |||
* [https://snack.expo.dev/@hsoubwiki/panresponder-with-animated-event-function-component مثال لمكون الدالة (Function Component)] | |||
<syntaxhighlight lang="javascript"> | |||
import React, { useRef } from "react"; | |||
import { Animated, View, StyleSheet, PanResponder, Text } from "react-native"; | |||
const App = () => { | |||
const pan = useRef(new Animated.ValueXY()).current; | |||
const panResponder = useRef( | |||
PanResponder.create({ | |||
onMoveShouldSetPanResponder: () => true, | |||
onPanResponderMove: Animated.event([ | |||
null, | |||
{ dx: pan.x, dy: pan.y } | |||
]), | |||
onPanResponderRelease: () => { | |||
Animated.spring(pan, { toValue: { x: 0, y: 0 } }).start(); | |||
} | |||
}) | |||
).current; | |||
return ( | |||
<View style={styles.container}> | |||
<Text style={styles.titleText}>Drag & Release this box!</Text> | |||
<Animated.View | |||
style={{ | |||
transform: [{ translateX: pan.x }, { translateY: pan.y }] | |||
}} | |||
{...panResponder.panHandlers} | |||
> | |||
<View style={styles.box} /> | |||
</Animated.View> | |||
</View> | |||
); | |||
} | |||
const styles = StyleSheet.create({ | |||
container: { | |||
flex: 1, | |||
alignItems: "center", | |||
justifyContent: "center" | |||
}, | |||
titleText: { | |||
fontSize: 14, | |||
lineHeight: 24, | |||
fontWeight: "bold" | |||
}, | |||
box: { | |||
height: 150, | |||
width: 150, | |||
backgroundColor: "blue", | |||
borderRadius: 5 | |||
} | |||
}); | |||
export default App; | |||
</syntaxhighlight> | </syntaxhighlight> | ||
* [https://snack.expo.dev/@hsoubwiki/panresponder-with-animated-event-class-component مثال لمكون الصنف (Class Component)] | |||
<syntaxhighlight lang="javascript"> | |||
import React, { Component } from "react"; | |||
import { Animated, View, StyleSheet, PanResponder, Text } from "react-native"; | |||
export default class App extends Component { | |||
pan = new Animated.ValueXY(); | |||
panResponder = PanResponder.create({ | |||
onMoveShouldSetPanResponder: () => true, | |||
onPanResponderMove: Animated.event([ | |||
null, | |||
{ dx: this.pan.x, dy: this.pan.y } | |||
]), | |||
onPanResponderRelease: () => { | |||
Animated.spring(this.pan, { toValue: { x: 0, y: 0 } }).start(); | |||
} | |||
}); | |||
render() { | |||
return ( | |||
<View style={styles.container}> | |||
<Text style={styles.titleText}>Drag & Release this box!</Text> | |||
<Animated.View | |||
style={{ | |||
transform: [{ translateX: this.pan.x }, { translateY: this.pan.y }] | |||
}} | |||
{...this.panResponder.panHandlers} | |||
> | |||
<View style={styles.box} /> | |||
</Animated.View> | |||
</View> | |||
); | |||
} | |||
} | |||
const styles = StyleSheet.create({ | |||
container: { | |||
flex: 1, | |||
alignItems: "center", | |||
justifyContent: "center" | |||
}, | |||
titleText: { | |||
fontSize: 14, | |||
lineHeight: 24, | |||
fontWeight: "bold" | |||
}, | |||
box: { | |||
height: 150, | |||
width: 150, | |||
backgroundColor: "blue", | |||
borderRadius: 5 | |||
} | |||
}); | |||
</syntaxhighlight> | |||
===الاستجابة لقيمة التحريك الحالية=== | ===الاستجابة لقيمة التحريك الحالية=== | ||
قد تلاحظ أنه لا توجد طريقة واضحة لقراءة القيمة الحالية أثناء التّحريك. وذلك لأن القيمة يُمكن أن تكون معروفة فقط في وقت التشغيل الأصيل (native runtime) بسبب تحسينات الأداء. إذا كنت بحاجة إلى تنفيذ شيفرة JavaScript تستجيب للقيمة الحالية، فهناك طريقتان: | قد تلاحظ أنه لا توجد طريقة واضحة لقراءة القيمة الحالية أثناء التّحريك. وذلك لأن القيمة يُمكن أن تكون معروفة فقط في وقت التشغيل الأصيل (native runtime) بسبب تحسينات الأداء. إذا كنت بحاجة إلى تنفيذ شيفرة JavaScript تستجيب للقيمة الحالية، فهناك طريقتان: | ||
* <code>spring.stopAnimation(callback)</code> دالّةٌ توقِف التحريك وتستدعي الدّالة <code>callback</code> المُمرَّرة مع تمرير القيمة النّهائيّة لها. هذه الطريقة مفيدة عند إجراء التحويل بين الإيماءات (gesture transitions). | * <code>spring.stopAnimation(callback)</code> دالّةٌ توقِف التحريك وتستدعي الدّالة <code>callback</code> المُمرَّرة مع تمرير القيمة النّهائيّة لها. هذه الطريقة مفيدة عند إجراء التحويل بين الإيماءات (gesture transitions). | ||
* <code>spring.addListener(callback)</code> دالّةٌ تستدعي <code>callback</code> بشكل غير متزامن (asynchronously) أثناء التّحريك، ما يوفِّر قيمةً حديثة. وهي طريقة مُفيدة لإحداث تغييرات في الحالة، يُمكن بهذا مثلًا جرُّ كائنٍ إلى خيارٍ جديد أثناء سحب المستخدم للكائن وتقريبه إلى الخيار، وهذا لأنّ هذه التغييرات الكبيرة في الحالة تكون أقل حساسيةً لتأخّر الإطارات الطّفيف مقارنةً بالإيماءات المستمرة مثل المسح التي تحتاج إلى تشغيلها بمُعدّل 60 إطارا في الثانية ( | * <code>spring.addListener(callback)</code> دالّةٌ تستدعي <code>callback</code> بشكل غير متزامن (asynchronously) أثناء التّحريك، ما يوفِّر قيمةً حديثة. وهي طريقة مُفيدة لإحداث تغييرات في الحالة، يُمكن بهذا مثلًا جرُّ كائنٍ إلى خيارٍ جديد أثناء سحب المستخدم للكائن وتقريبه إلى الخيار، وهذا لأنّ هذه التغييرات الكبيرة في الحالة تكون أقل حساسيةً لتأخّر الإطارات الطّفيف مقارنةً بالإيماءات المستمرة مثل المسح التي تحتاج إلى تشغيلها بمُعدّل 60 إطارا في الثانية (60fps). | ||
صُمِّمَت واجهة <code>Animated</code> لتكون قابلة للسَّلسَلة (serializable) بالكامل لِيُمكن تشغيل التّحريك بأداءٍ عالٍ، بغض النظر عن حلقة أحداث JavaScript العادية (JavaScript event loop). يؤثّر هذا على الواجهة البرمجيّة، لذا تذكّر هذا إذا بدا لك أنّ إنجاز أمرٍ أصعبُ ممّا لو كان النظام متزامنًا تمامًا. انظر [[ReactNative/animated|الدّالة <code>Animated.Value.addListener</code>]] كطريقة للتغلب على بعض هذه القيود، لكن توخّ الحذر عند استخدامها، فقد يكون لها آثار على الأداء مستقبلًا. | |||
===استخدام المشغل الأصيل (native driver)=== | ===استخدام المشغل الأصيل (native driver)=== | ||
صُمِّمَت واجهة <code>Animated</code> لتكون قابلة للسَّلسَلة. باستخدام مُشغّل التحريكات الأصيل، يُرسل الإطار كل ما يتعلّق بالتحريك إلى اللغة الأصيلة قبل تشغيل التّحريك، ما يسمح للشّيفرة الأصيلة بتنفيذ التّحريك على سلسلة واجهة المستخدم (UI thread) دون الحاجة إلى المرور عبر الجسر (الجسرُ الذي يربط بين شيفرة JavaScript وشيفرة المنصّة الأصيلة) عند كل إطار. عند بدء التّحريك، يُمكن تجميد (block) سلسلة JavaScript دون التأثير على التحريك. | صُمِّمَت واجهة <code>Animated</code> لتكون قابلة للسَّلسَلة. باستخدام مُشغّل التحريكات الأصيل، يُرسل الإطار كل ما يتعلّق بالتحريك إلى اللغة الأصيلة قبل تشغيل التّحريك، ما يسمح للشّيفرة الأصيلة بتنفيذ التّحريك على سلسلة واجهة المستخدم (UI thread) دون الحاجة إلى المرور عبر الجسر (الجسرُ الذي يربط بين شيفرة JavaScript وشيفرة المنصّة الأصيلة) عند كل إطار. عند بدء التّحريك، يُمكن تجميد (block) سلسلة JavaScript دون التأثير على التحريك. | ||
سطر 243: | سطر 625: | ||
استخدام المُشغِّل الأصيل للتّحريكات العادية بسيط للغاية. ما عليك سوى إضافة الخيار <code>useNativeDriver: true</code> إلى إعداد التحريك عند تشغيله: | استخدام المُشغِّل الأصيل للتّحريكات العادية بسيط للغاية. ما عليك سوى إضافة الخيار <code>useNativeDriver: true</code> إلى إعداد التحريك عند تشغيله: | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
Animated.timing(this.state.animatedValue, { | Animated.timing(this.state.animatedValue, { | ||
toValue: 1, | toValue: 1, | ||
duration: 500, | duration: 500, | ||
useNativeDriver: true | useNativeDriver: true // <-- أضف هذا السطر | ||
}).start(); | }).start(); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
سطر 259: | سطر 642: | ||
{ | { | ||
nativeEvent: { | nativeEvent: { | ||
contentOffset: {y: this.state.animatedValue} | contentOffset: { y: this.state.animatedValue } | ||
} | } | ||
} | } | ||
], | ], | ||
{useNativeDriver: true} | { useNativeDriver: true } // <-- أضف هذا السّطر | ||
)}> | )}> | ||
{content} | {content} | ||
سطر 271: | سطر 654: | ||
====محاذير==== | ====محاذير==== | ||
ليس كل ما يمكنك إنجازه بواجهة Animated مدعومًا حاليًا من المُشغّل الأصيل. القيد الرئيسي هو أنه يمكنك فقط تحريك الخاصيات غير التخطيطية (non-layout properties): ستَعمَل أمورٌ مثل التحويل (<code>transform</code>) والتعتيم (<code>opacity</code>)، لكن لا يمكن استخدام خاصيّات <code>flexbox</code> و<code>position</code>. عند استخدام الدّالة Animated. | ليس كل ما يمكنك إنجازه بواجهة <code>Animated</code> مدعومًا حاليًا من المُشغّل الأصيل. القيد الرئيسي هو أنه يمكنك فقط تحريك الخاصيات غير التخطيطية (non-layout properties): ستَعمَل أمورٌ مثل التحويل (<code>transform</code>) والتعتيم (<code>opacity</code>)، لكن لا يمكن استخدام خاصيّات <code>flexbox</code> و<code>position</code>. عند استخدام الدّالة <code>Animated.event</code>، فستعمل فقط مع الأحداث المباشرة (direct events) ولن تعمل مع الأحداث التي تنتقل إلى العناصر الأب (أو ما يُسمّى بغَليان الأحداث: event bubbling). ما يعني أنه لا يعمل مع واجهة <code>PanResponder</code> ولكنه يعمل مع أحداثٍ مثل <code>ScrollView#onScroll</code>. | ||
يُمكن للتّحريك أثناء اشتغاله منع مكوناتِ <code>VirtualizedList</code> من تصيير المزيد من الصفوف. إذا كنت بحاجة إلى تشغيل تحريك طويل أو تحريك مُتكرّرٍ أثناء مرور المستخدم عبر قائمةٍ ما، فيمكنك استخدام الخيار <code>isInteraction: false</code> في إعداد التّحريك الخاص بك لمنع حدوث هذه المشكلة. | يُمكن للتّحريك أثناء اشتغاله منع مكوناتِ <code>VirtualizedList</code> من تصيير المزيد من الصفوف. إذا كنت بحاجة إلى تشغيل تحريك طويل أو تحريك مُتكرّرٍ أثناء مرور المستخدم عبر قائمةٍ ما، فيمكنك استخدام الخيار <code>isInteraction: false</code> في إعداد التّحريك الخاص بك لمنع حدوث هذه المشكلة. | ||
===تذكير=== | ===تذكير=== | ||
عند استخدام أنماط التحويل (transform styles) مثل rotateY و rotateX وغير ذلك، فتأكّد من وجود نمط التحويل <code>perspective</code>. إذ قد لا تُعرَض بعض التّحريكات على نظام Android دونه في الوقت الحالي. مثال: | عند استخدام أنماط التحويل (transform styles) مثل <code>rotateY</code> و <code>rotateX</code> وغير ذلك، فتأكّد من وجود نمط التحويل <code>perspective</code>. إذ قد لا تُعرَض بعض التّحريكات على نظام Android دونه في الوقت الحالي. مثال: | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
<Animated.View | <Animated.View | ||
style={{ | style={{ | ||
transform: [ | transform: [ | ||
{scale: this.state.scale}, | { scale: this.state.scale }, | ||
{rotateY: this.state.rotateY}, | { rotateY: this.state.rotateY }, | ||
{perspective: 1000} | { perspective: 1000 } // لن يعمل التحريك على نظام أندرويد دون هذا السّطر | ||
] | ] | ||
}} | }} | ||
/> | /> | ||
سطر 294: | سطر 677: | ||
* [https://github.com/facebook/react-native/blob/master/RNTester/js/NativeAnimationsExample.js NativeAnimationsExample] | * [https://github.com/facebook/react-native/blob/master/RNTester/js/NativeAnimationsExample.js NativeAnimationsExample] | ||
==واجهة <code>LayoutAnimation</code> | ==واجهة <code>LayoutAnimation</code> البرمجية== | ||
تُتيح لك واجهة LayoutAnimation البرمجيّة إعداد تحريكاتِ <code>create</code> و<code>update</code> بشكل عامّ (globally)، والتي ستُستَخدَم لجميع | تُتيح لك واجهة <code>[[ReactNative/layoutanimation|LayoutAnimation]]</code> البرمجيّة إعداد تحريكاتِ <code>create</code> و<code>update</code> بشكل عامّ (globally)، والتي ستُستَخدَم لجميع الواجهات (views) في دورة التصيير (render) والتخطيط (layout) التالية. هذا مُفيد لإجراء تحديثاتٍ لتخطيط flexbox دون بذل جهدِ قياس أو حساب خاصيّاتٍ معينة لتحريكها مباشرةً، وهذا مفيدٌ بشكل خاصّ عندما تؤثر تغييرات التخطيط على المُكوِّنات الأجداد، على سبيل المثال، إنشاء مُكوِّنِ "اقرأ المزيد" يوسِّع مُكوّنًا أبًا ويزيد حجمه ويدفع الصّف الموجود أسفله سيتطلب تنسيقًا واضحًا ومُفصّلًا بين المكونات لتحريكها كلها بشكل متزامن. | ||
لاحظ أنه على الرغم من أنّ واجهة LayoutAnimation مفيدةٌ جدًّا، إلا أنها تُوفر تحكّمًا أقلّ بكثيرٍ من واجهة <code>Animated</code> ومكتبات تحريكٍ أخرى، لذا فقد تحتاج إلى استخدام طريقة أخرى إن لم تتمكن من الحصول على النتيجة المرغوبة باستخدام واجهة <code>LayoutAnimation</code>. | لاحظ أنه على الرغم من أنّ واجهة <code>LayoutAnimation</code> مفيدةٌ جدًّا، إلا أنها تُوفر تحكّمًا أقلّ بكثيرٍ من واجهة <code>Animated</code> ومكتبات تحريكٍ أخرى، لذا فقد تحتاج إلى استخدام طريقة أخرى إن لم تتمكن من الحصول على النتيجة المرغوبة باستخدام واجهة <code>LayoutAnimation</code>. | ||
ملاحظة: لتعمل الواجهة في نظام Android، ستحتاج إلى ضبط الخيارين التاليين في مُدير واجهة المستخدم <code>UIManager</code>: | ملاحظة: لتعمل الواجهة في نظام Android، ستحتاج إلى ضبط الخيارين التاليين في مُدير واجهة المستخدم <code>UIManager</code>: | ||
سطر 306: | سطر 689: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
مثال: | مثال ([https://snack.expo.dev/@hsoubwiki/layoutanimations-api تجربة حية]): | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
سطر 331: | سطر 714: | ||
_onPress = () => { | _onPress = () => { | ||
// تحريك التّحديث | // تحريك التّحديث | ||
LayoutAnimation.spring(); | LayoutAnimation.spring(); | ||
this.setState({w: this.state.w + 15, h: this.state.h + 15}) | this.setState({w: this.state.w + 15, h: this.state.h + 15}) | ||
سطر 378: | سطر 761: | ||
==ملاحظات إضافية== | ==ملاحظات إضافية== | ||
===<code>requestAnimationFrame</code>=== | ===<code>requestAnimationFrame</code>=== | ||
<code>requestAnimationFrame</code> شيفرة إملاءٍ (polyfill) تُستخدَم في | <code>requestAnimationFrame</code> شيفرة إملاءٍ (polyfill) تُستخدَم في الويب لتعويض نقص دعم المتصفحات، وقد تكون مألوفة لك. تقبل دالّةً كمُعاملٍ وحيدٍ وتستدعي الدّالة المُمرَّرة قبل إعادة الرسم (repaint) التّالي. وهي لَبِنَةٌ أساسيةٌ للتّحريكات تكمن وراء جميع واجهات التحريكات البرمجيّة المكتوبة بلغة <nowiki/>[[JavaScript]]. لا تحتاج عمومًا إلى استدعائها بنفسك لأنّ واجهات التحريكات البرمجيّة ستتكلّف بإدارة تحديثات الأطر (frame updates) عوضًا عنك. | ||
===<code>setNativeProps</code>=== | ===<code>setNativeProps</code>=== | ||
كما هو مذكور في قسم المعالجة المباشرة (Direct Manipulation)، تسمح لنا الدالّة <code>setNativeProps</code> بتعديل خاصيّات المكوّنات المدعومة من المنصّة الأصيلة (أي المكوّنات التي تعتمد على | كما هو مذكور في قسم المعالجة المباشرة (Direct Manipulation)، تسمح لنا الدالّة <code>setNativeProps</code> بتعديل خاصيّات المكوّنات المدعومة من المنصّة الأصيلة (أي المكوّنات التي تعتمد على الواجهات الأصيلة [native views]، على عكس المكونات المركبّة [composite components]) مباشرةً، دون الحاجة إلى استدعاء <code>setState</code> وإعادة تصيير تسلسل المُكوّنات الهرميّ. | ||
يُمكننا الاعتماد على هذه الميّزة في مثال الارتداد لتحديث المقياس، يُمكن لهذا أن يُساعد إذا كان المُكوّن الذي نُحدّثه متداخلًا تداخلًا عميقًا ولم يُحسَّن باستخدام <code>shouldComponentUpdate</code>. | يُمكننا الاعتماد على هذه الميّزة في مثال الارتداد لتحديث المقياس، يُمكن لهذا أن يُساعد إذا كان المُكوّن الذي نُحدّثه متداخلًا تداخلًا عميقًا ولم يُحسَّن باستخدام <code>shouldComponentUpdate</code>. | ||
سطر 389: | سطر 772: | ||
* [https://facebook.github.io/react-native/docs/animations صفحة Animations في توثيق React Native الرسمي.] | * [https://facebook.github.io/react-native/docs/animations صفحة Animations في توثيق React Native الرسمي.] | ||
[[تصنيف:ReactNative]] | [[تصنيف:ReactNative]] | ||
[[تصنيف:React Native Docs]] |
المراجعة الحالية بتاريخ 13:44، 9 أكتوبر 2021
التحريكات (Animations) مهمة جدًا لتجربة مستخدمٍ رائعة. يجب أن تتغلب الأشياء الثابتة على الجمود عندما تبدأ في التحرك. تملك الأجسام المتحركة زخمًا (أو قوّة دفعٍ: momentum) ونادراً ما تتوقف على الفور. تسمح لك التحريكات بإنشاء حركات فيزيائيّةٍ مُقنعةٍ في واجهة تطبيقك.
يوفر React Native نظامي تحريك يُكمِّلان بعضهما البعض:
- واجهة
Animated
للتحكم الحُبَيْبِيّ (granular) والتفاعليّ بقيمٍ محددة. - واجهة
LayoutAnimation
للتعامل مع تحريك التخطيط العام (global layout).
واجهة Animated
البرمجية
صُمِّمَت واجهة Animated
البرمجيّة لتسهيل التعبير عن مجموعة متنوعة من أنماط التحريك والتفاعل بطريقة فائقة الأداء. تركّز واجهة Animated
على العلاقات التعريفية بين المدخلات (inputs) والمخرجات (outputs)، مع وجود تحويلات قابلة للضّبط بينهما، إضافة إلى تابعي start
وstop
بسيطين للتحكم في تنفيذ التّحريك على أساس الوقت.
تُصدّر واجهة Animated
أربعة أنواع من المكونات القابلة للتّحريك: View
، وText
، وImage
، وScrollView
، ويمكنك كذلك إنشاء مُكوّنات خاصّة بك باستخدام التّابع Animated.createAnimatedComponent()
.
إليك مثال فيه تبْزُغُ (fades in) واجهة حاوية ببطء عندما تصيَّر أي تضاف إلى الشاشة (تجربة حية):
import React, { useRef, useEffect } from 'react';
import { Animated, Text, View } from 'react-native';
const FadeInView = (props) => {
const fadeAnim = useRef(new Animated.Value(0)).current // القيمة الابتدائية للعتامة opacity: 0
React.useEffect(() => {
Animated.timing(
fadeAnim,
{
toValue: 1,
duration: 10000,
}
).start();
}, [fadeAnim])
return (
<Animated.View // عرضٌ مُحرَّكٌ خاصّ
style={{
...props.style,
opacity: fadeAnim, // ربط العتامَة بالقيمة المُحرَّكة
}}
>
{props.children}
</Animated.View>
);
}
// يُمكنك الآن استخدام المكوّن
// `FadeInView`
// كعرضٍ بدلًا من
// `View`
// في مُكوّناتك
export default () => {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<FadeInView style={{width: 250, height: 50, backgroundColor: 'powderblue'}}>
<Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>Fading in</Text>
</FadeInView>
</View>
)
}
لننظر في تفاصيل الشيفرة أعلاه. في الدّالة البانية (constructor) للمكوّن FadeInView
، نُهيّئُ قيمةَ Animated.Value
جديدةً تُمثِّل قيمةً مُتحرِّكةً ونُسمِّيها fadeAnim
كجزء من الحالة (state
). تُربَط خاصيةُ عتامةٍ (opacity) على الواجهة View
مع هذه القيمة المتحركة. وراء الكواليس، تُستَخرَج القيمة الرقمية وتُستخدَم لضبط العتامة.
عندما يوصَل المُكوّن تُضبَطُ العتامة إلى 0. وبعد ذلك تبدأ حركة متسارعة (easing animation) تطبَّق على القيمة المتحركة fadeAnim
، ما سيُحدِّث جميع القيم المربوطة التابعة لها (في هذه الحالة، هناك قيمة مربوطةٌ واحدةٌ فقط تُمثِّل العتامة) على كل إطارٍ (frame) أثناء نقل القيمة إلى القيمة النهائية 1.
يتم هذا بطريقة محسّنةٍ أسرعَ من استدعاءِ setState
لتغيير الحالة وإعادة التصيير. ولأنّ الضبطَ تعريفيٌّ (declarative configuration) بأكمله، فسنكون قادرين على إجراء مزيد من التحسينات التي تقوم بسَلْسَلَةِ (serialize) الضّبط وتشغيل التحريك على سِلسِلَةٍ ذات أولوية عالية (high-priority thread).
ضبط التحريكات
يُمكن ضبط التحريكات بعدّة معاملاتٍ وخيارات. إذ يمكن تعديل دوال التسارع (easing functions) مخصّصة ومحددة مسبقًا، وتعديل التأخير (delay)، والمُدَدِ (durations)، وعوامل الِاضْمِحْلَالِ (decay factors)، وثوابت الوَثْبِ (spring constants)، وغير ذلك حسب نوع التّحريك.
تُوفّر واجهة Animated
العديد من أنواع التّحريك، وأكثرها استخدامًا هو التحريكُ Animated.timing()
، الذي يدعم تحريك قيمةٍ بمرور الوقت باستخدام إحدى دوال التّخفيف المُعرَّفة مُسبقًا (predefined)، أو يمكنك استخدام دوال خاصّة بك. عادةً ما تُستخدَم دوال التّخفيف في التّحريك لإظهار تسارعٍ تدريجيّ للأشياء ولإبطائها.
افتراضيًّا، سيستخدم التّابعُ timing
منحنى ظهورٍ مُخفَّف واختفاء مُخفَّف easeInOut، ما يُظهر تسارعًا تدريجيًّا إلى السرعة الكاملة ويختتم بتباطؤٍ تدريجيّ إلى التوقف. يمكنك تحديد دالّة تسارع مختلفة عبر تمريرها إلى المُعامل easing
. يُمكنك كذلك تحديد مدة duration
مخصّصةٍ، أو تأخيرٍ delay
قبل بدء التّحريك.
على سبيل المثال، يُمكننا إنشاء تحريك يدوم لثانيّتين لكائنٍ يرتدّ (backs up) ارتدادًا طفيفًا قبل الانتقال إلى موضعه النهائي كما يلي:
Animated.timing(this.state.xPosition, {
toValue: 100,
easing: Easing.back(),
duration: 2000
}).start();
انظر قسم ضبط التحريكات في توثيق واجهة Animated
للاستزادة حول جميع خيارات الضّبط المدعومة.
تركيب التحريكات
يمكن دمج التحريكات وتشغيلها بالتسلسل أو بالتوازي. يمكن تشغيل التحريكات التسلسلية فور انتهاء التحريك الذي يسبقها، أو يمكن تشغيلها بعد مدّة تأخير محددة. توفر واجهة Animated
عدة توابع لهذا، مثل sequence()
، وdelay()
، كل تابعٍ يأخذ ببساطةٍ مصفوفةً من التحريكات المرغوب تشغيلها، ويستدعي تلقائيًّا start()
، أو stop()
حسب الحاجة.
على سبيل المثال، يهبط (coasts) التحريك التالي تدريجيًّا إلى نقطة توقّف، ثم يرتدّ واثبًا (springs back) أثناء الالتفاف (twirling) بالتوازي:
Animated.sequence([
// تلاشى، ثمّ ارتدّ إلى نقطة البداية واِلْتفّ
Animated.decay(position, {
// اهبِط إلى نُقطَة توقّف
velocity: { x: gestureState.vx, y: gestureState.vy }, // velocity from gesture release
deceleration: 0.997
}),
Animated.parallel([
// بعد التّلاشي (بالتوازي)
Animated.spring(position, {
toValue: { x: 0, y: 0 } // return to start
}),
Animated.timing(twirl, {
// and twirl
toValue: 360
})
])
]).start(); // ابدأ سلسلة التحريكات
إذا أُوقِفَ تحريكٌ أو قُوطِع، فستُوقَف جميع التحريكات الأخرى في المجموعة كذلك. يحتوي التّابع Animated.parallel
خيارًا باسم stopTogether
يمكن ضبطُ قيمتِه إلى false
لتعطيل هذا السلوك.
لقائمةٍ كاملة بأساليب التركيب، انظر قسم تركيب التّحريكات في توثيق واجهة Animated
البرمجيّة.
دمج القيم المتحركة
يمكنك الجمع بين قيمتين متحركتين عن طريق الجمع أو الضرب أو القسمة أو عمليّة باقي القسمة (modulo) لإنشاء قيمة متحركة جديدة.
هناك بعض الحالات التي تحتاج فيها قيمة متحرِّكةٌ إلى قلب قيمة متحركة أخرى بغرض حساب التحريك. مثال على ذلك هو قلب مقياسٍ (من 2x إلى 0.5x):
const a = new Animated.Value(1);
const b = Animated.divide(1, a);
Animated.spring(a, {
toValue: 2
}).start();
الاستيفاء (Interpolation)
يمكن تمرير كل خاصية عبر استيفاءٍ أولاً. يربط الاستيفاء مجالات المدخلات (input ranges) مع مجالات المخرجات (output ranges)، وعادة ما يستخدم استيفاءٌ خطّي، ولكنّ هذه الطريقة تدعم كذلك دوال التسارع. افتراضيًّا، سيُستكمَل المنحنى استقرائيًّا (extrapolate) خارج المجالات المعطاة، لكن يمكنك شدّ (clamp) قيمة المُخرج كذلك كي لا تخرج عن حدود المجالات المُعطاة.
هذا مثال على ربطٍ بسيط لتحويل المجال 0-1 إلى المجال 0-100:
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100]
});
على سبيل المثال، قد ترغب بنقل قيمة التحريك Animated.Value
الخاصة بك من 0 إلى 1، مع تحريك الموضع من 150px
إلى 0px
و العتامة من 0 إلى 1. يمكن القيام بذلك بسهولة عن طريق تعديل النمط style
من المثال أعلاه هكذا:
style={{
opacity: this.state.fadeAnim, // ربط القيمة المتحرّكة مُباشرةً
transform: [{
translateY: this.state.fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
}],
}}
يدعم التّابع interpolate()
كذلك شرائح مجالاتٍ متعددة، ما يُسهل تحديد المناطق الهامدة (dead zones) وحيلٍ أخرى.
على سبيل المثال، للحصول على علاقة نفيٍ (negation relationship) عند النّقطة -300 تنتقِل إلى 0 عند النّقطة -100، ثم تنتقِل إلى 1 عند النّقطة 0، ثم تعود مُجدّدًا إلى الصفر عند النّقطة 100 ثمّ تُـتبَع بمنطقة هامدة تبقى عند النّقطة 0 لما وراء ذلك، فستكون الشيفرة مشابهة لما يلي:
value.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0]
});
ما سيُنشئ ترابطات كالتالي:
Input | Output
------|-------
-400| 450
-300| 300
-200| 150
-100| 0
-50| 0.5
0| 1
50| 0.5
100| 0
101| 0
200| 0
يدعم التّابع interpolate()
كذلك ربط القيم إلى سلاسل نصيّة، ما يسمح لك بتحريك الألوان والقيم ذات الوحدات (units). مثلًا، إن أردت تحريك دورانٍ فستكتب ما يلي:
value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg']
});
يدعم التّابع interpolate()
كذلك دوال تسارع/تخفيف محدّدة، والتي يوجد عدد منها في وحدة Easing
. إضافةً إلى أنّ للتّابعِ interpolate()
سلوكًا قابلًا للضّبط لاستقراء (extrapolation) المجال المُخرَج outputRange
. يمكنك ضبط الاستقراء عبر ضبط خيارات extrapolate
، وextrapolateLeft
، وextrapolateRight
. القيمة الافتراضية هي extend
للتوسيع، لكن يمكنك استخدام clamp
لمنع القيمة المُخرجَة من تجاوز مجال outputRange
.
تتبع القيم الديناميكية
يمكن للقيم المتحركة تَتبّعُ القيم الأخرى كذلك. ما عليك سوى تعيين قيمةِ toValue
خاصّةٍ بتحريكٍ إلى قيمة متحرّكة أخرى بدلاً من عددٍ عادي. على سبيل المثال، يمكن إنجاز تحريك "Chat Heads" كذلك المستخدم في تطبيق Messenger على Android عبر تثبيت spring()
على قيمة متحركة أخرى، أو باستعمال timing()
ومدّة duration
تُساوي 0 للتَتبّعٍ مُشدَّد. ويمكن كذلك إنجاز التّحريك عبر تركيب التحريكات بالاستيفاء:
Animated.spring(follower, { toValue: leader }).start();
Animated.timing(opacity, {
toValue: pan.x.interpolate({
inputRange: [0, 300],
outputRange: [1, 0]
})
}).start();
ستُنجَز القيم المتحركة leader
وfollower
باستخدامAnimated.ValueXY()
. القيمةValueXY
طريقةٌ سهلة للتعامل مع التفاعلات ثنائية الأبعاد، مثل المسح (panning) أو السحب (dragging). وهي غلافٌ (wrapper) بسيط يحتوي بشكل أساسي على نسختي Animated.Value
وبعض الدوال المساعدة التي تستدعى عبرها، مما يجعل ValueXY
بديلًا للقيمة Value
في العديد من الحالات. ما يسمح لنا بتتبّع القيمة x و y في المثال أعلاه.
تتبع الإيماءات
يمكن ربط الإيماءات (Gestures) مثل المسح (panning) أو التمرير (scrolling)، وأحداثٍ (events) أخرى مباشرةً بالقيم المتحركة باستخدام Animated.event
. يتم ذلك باستخدام بنية ربطٍ منظمة بحيث يمكن استخراج القيم من كائنات الأحداث (event objects) المعقدة. المستوى الأول هو مصفوفة للسماح بالرّبط عبر عدة مُعاملات، وتحتوي هذه المصفوفة على كائنات متداخلة.
على سبيل المثال، عند التّعامل مع إيماءات التمرير الأفقي (horizontal scrolling)، يمكنك القيام بما يلي لربط event.nativeEvent.contentOffset.x
بقيمةٍ مُتحرِّكة (تُهيّئ بالدالة Animated.Value
) باسم scrollX
:
onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{ nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}
ينفّذ المثال التالي سير تمرير أفقي (horizontal scrolling carousel) حيث تتحرك مؤشرات موضع التمرير باستخدام Animated.event
المستخدم في واجهة التمرير ScrollView
مثال واجهة تمرير مع حدث التحريك
import React, { useRef } from "react";
import {
SafeAreaView,
ScrollView,
Text,
StyleSheet,
View,
ImageBackground,
Animated,
useWindowDimensions
} from "react-native";
const images = new Array(6).fill('https://images.unsplash.com/photo-1556740749-887f6717d7e4');
const App = () => {
const scrollX = useRef(new Animated.Value(0)).current;
const { width: windowWidth } = useWindowDimensions();
return (
<SafeAreaView style={styles.container}>
<View style={styles.scrollContainer}>
<ScrollView
horizontal={true}
style={styles.scrollViewStyle}
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={Animated.event([
{
nativeEvent: {
contentOffset: {
x: scrollX
}
}
}
])}
scrollEventThrottle={1}
>
{images.map((image, imageIndex) => {
return (
<View
style={{ width: windowWidth, height: 250 }}
key={imageIndex}
>
<ImageBackground source={{ uri: image }} style={styles.card}>
<View style={styles.textContainer}>
<Text style={styles.infoText}>
{"Image - " + imageIndex}
</Text>
</View>
</ImageBackground>
</View>
);
})}
</ScrollView>
<View style={styles.indicatorContainer}>
{images.map((image, imageIndex) => {
const width = scrollX.interpolate({
inputRange: [
windowWidth * (imageIndex - 1),
windowWidth * imageIndex,
windowWidth * (imageIndex + 1)
],
outputRange: [8, 16, 8],
extrapolate: "clamp"
});
return (
<Animated.View
key={imageIndex}
style={[styles.normalDot, { width }]}
/>
);
})}
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
scrollContainer: {
height: 300,
alignItems: "center",
justifyContent: "center"
},
card: {
flex: 1,
marginVertical: 4,
marginHorizontal: 16,
borderRadius: 5,
overflow: "hidden",
alignItems: "center",
justifyContent: "center"
},
textContainer: {
backgroundColor: "rgba(0,0,0, 0.7)",
paddingHorizontal: 24,
paddingVertical: 8,
borderRadius: 5
},
infoText: {
color: "white",
fontSize: 16,
fontWeight: "bold"
},
normalDot: {
height: 8,
width: 8,
borderRadius: 4,
backgroundColor: "silver",
marginHorizontal: 4
},
indicatorContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center"
}
});
export default App;
import React, { Component } from "react";
import {
SafeAreaView,
ScrollView,
Text,
StyleSheet,
View,
ImageBackground,
Animated,
Dimensions
} from "react-native";
const images = new Array(6).fill('https://images.unsplash.com/photo-1556740749-887f6717d7e4');
const window = Dimensions.get("window");
export default class App extends Component {
scrollX = new Animated.Value(0);
state = {
dimensions: {
window
}
};
onDimensionsChange = ({ window }) => {
this.setState({ dimensions: { window } });
};
componentDidMount() {
Dimensions.addEventListener("change", this.onDimensionsChange);
}
componentWillUnmount() {
Dimensions.removeEventListener("change", this.onDimensionsChange);
}
render() {
const windowWidth = this.state.dimensions.window.width;
return (
<SafeAreaView style={styles.container}>
<View style={styles.scrollContainer}>
<ScrollView
horizontal={true}
style={styles.scrollViewStyle}
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={Animated.event([
{
nativeEvent: {
contentOffset: {
x: this.scrollX
}
}
}
])}
scrollEventThrottle={1}
>
{images.map((image, imageIndex) => {
return (
<View
style={{
width: windowWidth,
height: 250
}}
key={imageIndex}
>
<ImageBackground source={{ uri: image }} style={styles.card}>
<View style={styles.textContainer}>
<Text style={styles.infoText}>
{"Image - " + imageIndex}
</Text>
</View>
</ImageBackground>
</View>
);
})}
</ScrollView>
<View style={styles.indicatorContainer}>
{images.map((image, imageIndex) => {
const width = this.scrollX.interpolate({
inputRange: [
windowWidth * (imageIndex - 1),
windowWidth * imageIndex,
windowWidth * (imageIndex + 1)
],
outputRange: [8, 16, 8],
extrapolate: "clamp"
});
return (
<Animated.View
key={imageIndex}
style={[styles.normalDot, { width }]}
/>
);
})}
</View>
</View>
</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
scrollContainer: {
height: 300,
alignItems: "center",
justifyContent: "center"
},
card: {
flex: 1,
marginVertical: 4,
marginHorizontal: 16,
borderRadius: 5,
overflow: "hidden",
alignItems: "center",
justifyContent: "center"
},
textContainer: {
backgroundColor: "rgba(0,0,0, 0.7)",
paddingHorizontal: 24,
paddingVertical: 8,
borderRadius: 5
},
infoText: {
color: "white",
fontSize: 16,
fontWeight: "bold"
},
normalDot: {
height: 8,
width: 8,
borderRadius: 4,
backgroundColor: "silver",
marginHorizontal: 4
},
indicatorContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center"
}
});
عند استخدام واجهة PanResponder
، يمكنك استخدام الشيفرة التالية لاستخراج مواضع x و y من gestureState.dx
و gestureState.dy
. نستخدم القيمة null
في الموضع الأول للمصفوفة، لأنّنا لا نحتاج إلّا إلى المتغير الثاني الذي يُمرَّر إلى معالج PanResponder
، والذي يُمثِّل حالة الإيماءة gestureState
.
onPanResponderMove={Animated.event(
[null, // تجاهل الحدث الأصيل
// استخرج dx و dy من gestureState
// 'pan.x = gestureState.dx, pan.y = gestureState.dy'مثل
{dx: pan.x, dy: pan.y}
])}
إليك مثال لواجهة PanResponder
مع حدث التحريك:
import React, { useRef } from "react";
import { Animated, View, StyleSheet, PanResponder, Text } from "react-native";
const App = () => {
const pan = useRef(new Animated.ValueXY()).current;
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null,
{ dx: pan.x, dy: pan.y }
]),
onPanResponderRelease: () => {
Animated.spring(pan, { toValue: { x: 0, y: 0 } }).start();
}
})
).current;
return (
<View style={styles.container}>
<Text style={styles.titleText}>Drag & Release this box!</Text>
<Animated.View
style={{
transform: [{ translateX: pan.x }, { translateY: pan.y }]
}}
{...panResponder.panHandlers}
>
<View style={styles.box} />
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
titleText: {
fontSize: 14,
lineHeight: 24,
fontWeight: "bold"
},
box: {
height: 150,
width: 150,
backgroundColor: "blue",
borderRadius: 5
}
});
export default App;
import React, { Component } from "react";
import { Animated, View, StyleSheet, PanResponder, Text } from "react-native";
export default class App extends Component {
pan = new Animated.ValueXY();
panResponder = PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null,
{ dx: this.pan.x, dy: this.pan.y }
]),
onPanResponderRelease: () => {
Animated.spring(this.pan, { toValue: { x: 0, y: 0 } }).start();
}
});
render() {
return (
<View style={styles.container}>
<Text style={styles.titleText}>Drag & Release this box!</Text>
<Animated.View
style={{
transform: [{ translateX: this.pan.x }, { translateY: this.pan.y }]
}}
{...this.panResponder.panHandlers}
>
<View style={styles.box} />
</Animated.View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
titleText: {
fontSize: 14,
lineHeight: 24,
fontWeight: "bold"
},
box: {
height: 150,
width: 150,
backgroundColor: "blue",
borderRadius: 5
}
});
الاستجابة لقيمة التحريك الحالية
قد تلاحظ أنه لا توجد طريقة واضحة لقراءة القيمة الحالية أثناء التّحريك. وذلك لأن القيمة يُمكن أن تكون معروفة فقط في وقت التشغيل الأصيل (native runtime) بسبب تحسينات الأداء. إذا كنت بحاجة إلى تنفيذ شيفرة JavaScript تستجيب للقيمة الحالية، فهناك طريقتان:
-
spring.stopAnimation(callback)
دالّةٌ توقِف التحريك وتستدعي الدّالة callback
المُمرَّرة مع تمرير القيمة النّهائيّة لها. هذه الطريقة مفيدة عند إجراء التحويل بين الإيماءات (gesture transitions). -
spring.addListener(callback)
دالّةٌ تستدعي callback
بشكل غير متزامن (asynchronously) أثناء التّحريك، ما يوفِّر قيمةً حديثة. وهي طريقة مُفيدة لإحداث تغييرات في الحالة، يُمكن بهذا مثلًا جرُّ كائنٍ إلى خيارٍ جديد أثناء سحب المستخدم للكائن وتقريبه إلى الخيار، وهذا لأنّ هذه التغييرات الكبيرة في الحالة تكون أقل حساسيةً لتأخّر الإطارات الطّفيف مقارنةً بالإيماءات المستمرة مثل المسح التي تحتاج إلى تشغيلها بمُعدّل 60 إطارا في الثانية (60fps).
صُمِّمَت واجهة Animated
لتكون قابلة للسَّلسَلة (serializable) بالكامل لِيُمكن تشغيل التّحريك بأداءٍ عالٍ، بغض النظر عن حلقة أحداث JavaScript العادية (JavaScript event loop). يؤثّر هذا على الواجهة البرمجيّة، لذا تذكّر هذا إذا بدا لك أنّ إنجاز أمرٍ أصعبُ ممّا لو كان النظام متزامنًا تمامًا. انظر الدّالة Animated.Value.addListener
كطريقة للتغلب على بعض هذه القيود، لكن توخّ الحذر عند استخدامها، فقد يكون لها آثار على الأداء مستقبلًا.
استخدام المشغل الأصيل (native driver)
صُمِّمَت واجهة Animated
لتكون قابلة للسَّلسَلة. باستخدام مُشغّل التحريكات الأصيل، يُرسل الإطار كل ما يتعلّق بالتحريك إلى اللغة الأصيلة قبل تشغيل التّحريك، ما يسمح للشّيفرة الأصيلة بتنفيذ التّحريك على سلسلة واجهة المستخدم (UI thread) دون الحاجة إلى المرور عبر الجسر (الجسرُ الذي يربط بين شيفرة JavaScript وشيفرة المنصّة الأصيلة) عند كل إطار. عند بدء التّحريك، يُمكن تجميد (block) سلسلة JavaScript دون التأثير على التحريك.
استخدام المُشغِّل الأصيل للتّحريكات العادية بسيط للغاية. ما عليك سوى إضافة الخيار useNativeDriver: true
إلى إعداد التحريك عند تشغيله:
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true // <-- أضف هذا السطر
}).start();
تتوافق القيم المتحركة مع مُشغّل تحريكات واحد فقط، لذا إذا كنت تستخدم المُشغِّل الأصيل عند بدء تحريك قيمة ما، فتأكّد من أن جميع التّحريكات على هذه القيمة تستخدم المُشغِّل الأصيل كذلك.
يعمل المُشغِّل الأصيل مع الدّالة Animated.event
كذلك . هذا مفيد بشكل خاص للتّحريكات التي تتّبع موضع التمرير (scroll position)، لأنّه بدون المُشغِّل الأصيل، سيتأخّر التّحريك دائمًا بإطارٍ واحدٍ بعد الإيماءة بسبب الطبيعة غير المتزامنة لإطار React Native.
<Animated.ScrollView // <-- استعمل عرض التمرير المُحرَّك كغلاف
scrollEventThrottle={1} // <-- الرّقم واحد يعني التّأكّد من أنّ جميع الأحداث تُلتَقط دون استثناء
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: { y: this.state.animatedValue }
}
}
],
{ useNativeDriver: true } // <-- أضف هذا السّطر
)}>
{content}
</Animated.ScrollView>
يمكنك رؤية قدرات المُشغّل الأصيل في تطبيق RNTester، عبر تحميل مثال Native Animated Example. يمكنك كذلك إلقاء نظرة على الشيفرة المصدريّة لتفهم كيف طُوّرَت هذه الأمثلة.
محاذير
ليس كل ما يمكنك إنجازه بواجهة Animated
مدعومًا حاليًا من المُشغّل الأصيل. القيد الرئيسي هو أنه يمكنك فقط تحريك الخاصيات غير التخطيطية (non-layout properties): ستَعمَل أمورٌ مثل التحويل (transform
) والتعتيم (opacity
)، لكن لا يمكن استخدام خاصيّات flexbox
وposition
. عند استخدام الدّالة Animated.event
، فستعمل فقط مع الأحداث المباشرة (direct events) ولن تعمل مع الأحداث التي تنتقل إلى العناصر الأب (أو ما يُسمّى بغَليان الأحداث: event bubbling). ما يعني أنه لا يعمل مع واجهة PanResponder
ولكنه يعمل مع أحداثٍ مثل ScrollView#onScroll
.
يُمكن للتّحريك أثناء اشتغاله منع مكوناتِ VirtualizedList
من تصيير المزيد من الصفوف. إذا كنت بحاجة إلى تشغيل تحريك طويل أو تحريك مُتكرّرٍ أثناء مرور المستخدم عبر قائمةٍ ما، فيمكنك استخدام الخيار isInteraction: false
في إعداد التّحريك الخاص بك لمنع حدوث هذه المشكلة.
تذكير
عند استخدام أنماط التحويل (transform styles) مثل rotateY
و rotateX
وغير ذلك، فتأكّد من وجود نمط التحويل perspective
. إذ قد لا تُعرَض بعض التّحريكات على نظام Android دونه في الوقت الحالي. مثال:
<Animated.View
style={{
transform: [
{ scale: this.state.scale },
{ rotateY: this.state.rotateY },
{ perspective: 1000 } // لن يعمل التحريك على نظام أندرويد دون هذا السّطر
]
}}
/>
أمثلة أخرى
يحتوي تطبيق RNTester على عدّة أمثلة تستعمل واجهة Animated
:
واجهة LayoutAnimation
البرمجية
تُتيح لك واجهة LayoutAnimation
البرمجيّة إعداد تحريكاتِ create
وupdate
بشكل عامّ (globally)، والتي ستُستَخدَم لجميع الواجهات (views) في دورة التصيير (render) والتخطيط (layout) التالية. هذا مُفيد لإجراء تحديثاتٍ لتخطيط flexbox دون بذل جهدِ قياس أو حساب خاصيّاتٍ معينة لتحريكها مباشرةً، وهذا مفيدٌ بشكل خاصّ عندما تؤثر تغييرات التخطيط على المُكوِّنات الأجداد، على سبيل المثال، إنشاء مُكوِّنِ "اقرأ المزيد" يوسِّع مُكوّنًا أبًا ويزيد حجمه ويدفع الصّف الموجود أسفله سيتطلب تنسيقًا واضحًا ومُفصّلًا بين المكونات لتحريكها كلها بشكل متزامن.
لاحظ أنه على الرغم من أنّ واجهة LayoutAnimation
مفيدةٌ جدًّا، إلا أنها تُوفر تحكّمًا أقلّ بكثيرٍ من واجهة Animated
ومكتبات تحريكٍ أخرى، لذا فقد تحتاج إلى استخدام طريقة أخرى إن لم تتمكن من الحصول على النتيجة المرغوبة باستخدام واجهة LayoutAnimation
.
ملاحظة: لتعمل الواجهة في نظام Android، ستحتاج إلى ضبط الخيارين التاليين في مُدير واجهة المستخدم UIManager
:
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);
مثال (تجربة حية):
import React from 'react';
import {
NativeModules,
LayoutAnimation,
Text,
TouchableOpacity,
StyleSheet,
View,
} from 'react-native';
const { UIManager } = NativeModules;
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);
export default class App extends React.Component {
state = {
w: 100,
h: 100,
};
_onPress = () => {
// تحريك التّحديث
LayoutAnimation.spring();
this.setState({w: this.state.w + 15, h: this.state.h + 15})
}
render() {
return (
<View style={styles.container}>
<View style={[styles.box, {width: this.state.w, height: this.state.h}]} />
<TouchableOpacity onPress={this._onPress}>
<View style={styles.button}>
<Text style={styles.buttonText}>Press me!</Text>
</View>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
box: {
width: 200,
height: 200,
backgroundColor: 'red',
},
button: {
backgroundColor: 'black',
paddingHorizontal: 20,
paddingVertical: 15,
marginTop: 15,
},
buttonText: {
color: '#fff',
fontWeight: 'bold',
},
});
يستخدم هذا المثال قيمةً مُحدَّدةً مُسبقًا، لكن يُمكنك تخصيص التّحريك حسب الحاجة، انظر LayoutAnimation.js
للمزيد من المعلومات.
ملاحظات إضافية
requestAnimationFrame
requestAnimationFrame
شيفرة إملاءٍ (polyfill) تُستخدَم في الويب لتعويض نقص دعم المتصفحات، وقد تكون مألوفة لك. تقبل دالّةً كمُعاملٍ وحيدٍ وتستدعي الدّالة المُمرَّرة قبل إعادة الرسم (repaint) التّالي. وهي لَبِنَةٌ أساسيةٌ للتّحريكات تكمن وراء جميع واجهات التحريكات البرمجيّة المكتوبة بلغة JavaScript. لا تحتاج عمومًا إلى استدعائها بنفسك لأنّ واجهات التحريكات البرمجيّة ستتكلّف بإدارة تحديثات الأطر (frame updates) عوضًا عنك.
setNativeProps
كما هو مذكور في قسم المعالجة المباشرة (Direct Manipulation)، تسمح لنا الدالّة setNativeProps
بتعديل خاصيّات المكوّنات المدعومة من المنصّة الأصيلة (أي المكوّنات التي تعتمد على الواجهات الأصيلة [native views]، على عكس المكونات المركبّة [composite components]) مباشرةً، دون الحاجة إلى استدعاء setState
وإعادة تصيير تسلسل المُكوّنات الهرميّ.
يُمكننا الاعتماد على هذه الميّزة في مثال الارتداد لتحديث المقياس، يُمكن لهذا أن يُساعد إذا كان المُكوّن الذي نُحدّثه متداخلًا تداخلًا عميقًا ولم يُحسَّن باستخدام shouldComponentUpdate
.
إذا رأيت أنّ التحريكات في تطبيقك تتجاوز بعض الأطر (بأداء أقلّ من 60 إطارًا في الثّانيّة)، فجرّب استخدام setNativeProps
أو shouldComponentUpdate
لتحسينها. أو يُمكنك تشغيل التحريكات على سلسلة واجهة المستخدم (UI thread) عوضًا عن سلسلة JavaScript باستخدام خيار useNativeDriver
. قد ترغب كذلك بتأجيل أي عمليّات تتطلّب الكثير من الحسابات إلى حين انتهاء التحريكات باستخدام مدير التفاعلات InteractionManager
. يُمكنك مراقبة معدّل الأطر باستخدام أداة "FPS Monitor" في قائمة المُطوّرين داخل التّطبيق (In-App Developer Menu).