حركات الإيماءات والنوابض
حركة طبيعية قائمة على الفيزياء
حركات الإيماءات والنوابض درس مجاني في Android Academy على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Android Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Android Academy 4 دروس في المجموع.
حركة تفاعلية فيزيائية
الرسوم المتحركة الأكثر إمتاعًا هي التي تستجيب لإصبع المستخدم وتمنح إحساس الأجسام الحقيقية، بما فيها من زخم وارتداد. ولإنشائها، تجمعون بين الإيماءات والرسوم المتحركة المعتمدة على الفيزياء.
يتناول هذا الدرس Animatable والنوابض وتلاشي الرمية وربطها بإيماءات السحب لإنشاء واجهة مستخدم طبيعية وتفاعلية.
Animatable: تحكم منخفض المستوى
بينما تستجيب animate*AsState للحالة، تمنحكم Animatable تحكمًا إجرائيًا. تحتفظون بكائن منها داخل remember وتحرّكونه من خلال coroutine باستخدام استدعاءات مثل animateTo أو snapTo.
وهذا هو الأساس للحركة التي تقودها الإيماءات.
@Composable
fun BounceOnClick() {
val scale = remember { Animatable(1f) }
val scope = rememberCoroutineScope()
Box(
Modifier
.graphicsLayer { scaleX = scale.value; scaleY = scale.value }
.clickable {
scope.launch {
scale.animateTo(1.3f, spring())
scale.animateTo(1f, spring())
}
}
)
}النوابض: dampingRatio وstiffness
تتحدد مواصفة spring بقيمتين:
- dampingRatio - مقدار الارتداد. كلما انخفضت القيمة زاد الارتداد؛ أما
1f(التخميد الحرج) فلا يتجاوز الهدف. - stiffness - سرعة الحركة. كلما ارتفعت القيمة، وصلت الحركة إلى الهدف بسرعة أكبر.
يوفّر Compose ثوابت مثل Spring.DampingRatioMediumBouncy وSpring.StiffnessLow، لذا نادرًا ما تحتاجون إلى استخدام أرقام مباشرة.
val spec = spring<Float>(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMedium
)
// later, inside a coroutine:
// offset.animateTo(targetValue = 0f, animationSpec = spec)اكتشاف إيماءات السحب
لجعل الحركة تفاعلية، اقرأ إدخال اللمس. يرسل Modifier.pointerInput مع detectDragGestures فروق السحب أثناء تحريك المستخدم إصبعه.
تجمع هذه الفروق في إزاحة وتطبقها على العنصر القابل للتركيب.
@Composable
fun Draggable() {
var offsetX by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
.size(80.dp)
.background(Color.Red)
.pointerInput(Unit) {
detectDragGestures { change, drag ->
change.consume()
offsetX += drag.x
}
}
)
}العودة بارتداد نابضي
اجمع بين السحب وAnimatable للحصول على تأثير «العودة». أثناء السحب، استخدم snapTo لوضع العنصر فورًا عند موضع الإصبع، وعند الإفلات استخدم animateTo(0f) مع نابض، لكي يرتد العنصر إلى موضعه الأصلي.
@Composable
fun SnapBack() {
val offsetX = remember { Animatable(0f) }
val scope = rememberCoroutineScope()
Box(
Modifier
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
.size(80.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, drag ->
change.consume()
scope.launch { offsetX.snapTo(offsetX.value + drag.x) }
},
onDragEnd = {
scope.launch { offsetX.animateTo(0f, spring()) }
}
)
}
)
}الرمي بالسرعة
تستمر حركات الرمي الحقيقية بعد رفع الإصبع. تتبع السرعة باستخدام VelocityTracker، ثم مررها إلى animateDecay لكي يستمر العنصر في الحركة تدريجيًا حتى يتوقف طبيعيًا باستخدام DecayAnimationSpec القائم على الاحتكاك.
val decay = rememberSplineBasedDecay<Float>()
// on drag end, with measured velocity:
// scope.launch {
// offsetX.animateDecay(
// initialVelocity = velocity,
// animationSpec = decay
// )
// }السحب للإغلاق باستخدام نقاط الارتكاز
بالنسبة إلى إيماءات السحب ذات المواضع الثابتة للاستقرار، مثل السحب للإغلاق، تعمل anchoredDraggable على جذب العنصر إلى نقاط ارتكاز محددة. تعلن عن الحالات ومواضعها بالبكسل، ويتولى Compose تطبيق الحركة النابضية بينها.
enum class DragValue { Start, End }
val state = remember {
AnchoredDraggableState(
initialValue = DragValue.Start,
anchors = DraggableAnchors {
DragValue.Start at 0f
DragValue.End at 300f
}
)
}الحركات اللانهائية والمتكررة
للحركة المستمرة، مثل نقطة نابضة أو تأثير لمعان التحميل، استخدم rememberInfiniteTransition مع infiniteRepeatable. عيّن RepeatMode إلى Reverse للحركة الترددية، أو إلى Restart للتكرار من البداية.
@Composable
fun Pulse() {
val transition = rememberInfiniteTransition(label = "pulse")
val scale by transition.animateFloat(
initialValue = 1f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(700),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
Box(Modifier.graphicsLayer { scaleX = scale; scaleY = scale })
}الجمع بين الإيماءة والنابض
النمط التفاعلي المميز هو: السحب للتحريك، ثم الإفلات للاستقرار. تتبع السرعة أثناء السحب، ثم نفّذ إما رميًا باستخدام animateDecay أو استقرارًا نحو نقطة ارتكاز باستخدام spring، وفقًا للمسافة أو السرعة.
وهذه بالضبط الطريقة التي تجعل الألواح السفلية وواجهات التمرير والبطاقات القابلة للإغلاق تبدو طبيعية جدًا.
// pattern:
// 1) onDrag -> animatable.snapTo(current + delta)
// 2) onDragEnd -> if (fast) animatable.animateDecay(velocity, decay)
// else animatable.animateTo(nearestAnchor, spring())الاستقرار النابضي باستخدام Kotlin خالص
لفهم كيفية استقرار النابض، إليك حلقة بسيطة مكتوبة بلغة Kotlin خالصة. تطبق الحلقة قوة استعادة باتجاه الهدف وتخفف السرعة في كل خطوة، وهي الفكرة نفسها التي ينفذها Compose في كل إطار.
fun main() {
var pos = 0f
var vel = 0f
val target = 200f
val stiffness = 0.2f
val damping = 0.75f
repeat(8) { step ->
vel = (vel + (target - pos) * stiffness) * damping
pos += vel
println("step $step pos=${"%.1f".format(pos)}")
}
}النقر والضغط والإيماءات المتعددة
السحب ليس الإيماءة الوحيدة. يوفر pointerInput أيضًا detectTapGestures للنقر والنقر المزدوج والضغط المطول، وdetectTransformGestures للتكبير بإيماءة القرص والتدوير.
يؤدي الضغط المطول هنا إلى تشغيل تغيير نابضي في الحجم، مع الجمع بين اكتشاف الإيماءات وAnimatable.
@Composable
fun LongPressScale() {
val scale = remember { Animatable(1f) }
val scope = rememberCoroutineScope()
Box(
Modifier
.graphicsLayer { scaleX = scale.value; scaleY = scale.value }
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
scope.launch { scale.animateTo(1.4f, spring()) }
},
onPress = {
tryAwaitRelease()
scope.launch { scale.animateTo(1f, spring()) }
}
)
}
)
}تحقق سريع
أنت تبني بطاقة قابلة للسحب، وينبغي أن ترتد إلى أصلها عند الإفلات. ما المجموعة التي تمثل الخيار الاصطلاحي في Compose؟
مراجعة: الإيماءات والنوابض
يمكنك الآن إنشاء حركة طبيعية وتفاعلية:
- يوفر
Animatableتحكمًا إجرائيًا تقوده القيم المعلّقة من خلالanimateTo/snapTo. - يُضبط
springباستخدامdampingRatio(الارتداد) وstiffness(السرعة). - يحوّل
pointerInput+detectDragGesturesاللمس إلى حركة، بينما يضيفanimateDecayحركة الرمي. - تجذب
anchoredDraggableالعنصر إلى مواضع ثابتة لتفاعلات السحب. - يشغّل
rememberInfiniteTransitionتأثيرات متكررة مثل النبض واللمعان.
وبذلك تكتمل أساسيات الحركات في Jetpack Compose. يمكنك الآن تحريك القيم والظهور والمحتوى والإيماءات بثقة.
الأسئلة الشائعة
هل درس «حركات الإيماءات والنوابض» مجاني؟
نعم — نص درس «حركات الإيماءات والنوابض» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Android Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Android Academy 4 دروس في المجموع.
ماذا ستتعلم في «حركات الإيماءات والنوابض»؟
حركة طبيعية قائمة على الفيزياء تتمرن على Android Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ Android Academy؟
لا تُشترط خبرة سابقة. Android Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.
كم من الوقت يستغرق درس «حركات الإيماءات والنوابض»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس Android Academy هذا؟
نعم. كل درس في Android Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- لماذا تهم الرسوم المتحركة
- أساسيات animate*AsState
- AnimatedVisibility والانتقالات
- حركات الإيماءات والنوابض