الدليل السريع للصور المتحركة في ميزة "الكتابة"

يتضمّن Compose العديد من آليات الصور المتحركة المضمّنة، وقد يكون من الصعب معرفة أيّ منها يجب اختياره. فيما يلي قائمة بحالات استخدام الرسوم المتحركة الشائعة. لمزيد من المعلومات التفصيلية حول المجموعة الكاملة من خيارات واجهة برمجة التطبيقات المختلفة المتاحة لك، يمكنك قراءة مستندات Compose Animation الكاملة.

إضافة حركة إلى خصائص الدوال المركّبة الشائعة

توفّر Compose واجهات برمجة تطبيقات ملائمة تتيح لك حلّ العديد من حالات الاستخدام الشائعة للصور المتحركة. يوضّح هذا القسم كيفية تحريك السمات الشائعة لدالة مركّبة.

تحريك الظهور / الاختفاء

دالة مركّبة خضراء تظهر وتختفي
الشكل 1. إضافة تأثيرات حركية لظهور عنصر واختفائه في عمود

استخدِم AnimatedVisibility لإخفاء دالة مركّبة أو إظهارها. يمكن للأطفال داخل AnimatedVisibility استخدام Modifier.animateEnterExit() للانتقال إلى الداخل أو الخارج.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

تتيح لك مَعلمتا الدخول والخروج في AnimatedVisibility ضبط سلوك الدالة المركّبة عند ظهورها واختفائها. يمكنك الاطّلاع على المستندات الكاملة لمزيد من المعلومات.

هناك خيار آخر لإضافة تأثيرات حركية إلى مستوى ظهور دالة مركّبة، وهو إضافة تأثيرات حركية إلى قيمة ألفا بمرور الوقت باستخدام animateFloatAsState:

var visible by remember {
    mutableStateOf(true)
}
val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f,
    label = "alpha"
)
Box(
    modifier = Modifier
        .size(200.dp)
        .graphicsLayer {
            alpha = animatedAlpha
        }
        .clip(RoundedCornerShape(8.dp))
        .background(colorGreen)
        .align(Alignment.TopCenter)
) {
}

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

تحريك شفافية دالة مركّبة
الشكل 2. تحريك شفافية دالة مركّبة

تحريك لون الخلفية

دالة مركّبة يتغيّر لون خلفيتها بمرور الوقت كصورة متحركة، حيث تتلاشى الألوان في بعضها البعض.
الشكل 3. تحريك لون خلفية الدالة المركّبة

val animatedColor by animateColorAsState(
    if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composable here
}

يوفّر هذا الخيار أداءً أفضل من استخدام Modifier.background(). يُعدّ Modifier.background() مقبولاً لإعداد لون لمرة واحدة، ولكن عند تحريك لون بمرور الوقت، قد يؤدي ذلك إلى إعادة إنشاء أكثر من اللازم.

لتحريك لون الخلفية بشكل لا نهائي، يُرجى الاطّلاع على قسم تكرار الحركة.

تحريك حجم دالة مركّبة

دالة مركّبة خضراء تحرّك تغيير حجمها بسلاسة
الشكل 4. دالة مركّبة تعرض صورة متحركة بسلاسة بين حجم صغير وحجم أكبر

تتيح لك Compose تحريك حجم الدوال المركّبة ببضع طرق مختلفة. استخدِم animateContentSize() للصور المتحركة بين تغييرات حجم الدوال المركّبة.

على سبيل المثال، إذا كان لديك مربّع يحتوي على نص يمكن توسيعه من سطر واحد إلى أسطر متعددة، يمكنك استخدام Modifier.animateContentSize() لتحقيق انتقال أكثر سلاسة:

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

يمكنك أيضًا استخدام AnimatedContent مع SizeTransform لوصف كيفية تغيير الحجم.

تحريك موضع الدالة المركّبة

دالة مركّبة خضراء تتحرك بسلاسة إلى الأسفل وإلى اليمين
الشكل 5. دالة مركّبة تتحرك بمقدار إزاحة

لتحريك موضع دالة مركّبة، استخدِم Modifier.offset{ } مع animateIntOffsetAsState().

var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = "offset"
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
        }
)

إذا أردت التأكّد من عدم رسم الدوال المركّبة فوق أو تحت دوال مركّبة أخرى عند إضافة تأثيرات حركية إلى الموضع أو الحجم، استخدِم Modifier.layout{ }. ينشر هذا المُعدِّل تغييرات الحجم والموضع إلى العنصر الرئيسي، ما يؤثر بعد ذلك في العناصر الفرعية الأخرى.

على سبيل المثال، إذا كنت تنقل Box داخل Column وتحتاج العناصر الثانوية الأخرى إلى الانتقال عند نقل Box، يمكنك تضمين معلومات الإزاحة مع Modifier.layout{ } على النحو التالي:

var toggled by remember {
    mutableStateOf(false)
}
val interactionSource = remember {
    MutableInteractionSource()
}
Column(
    modifier = Modifier
        .padding(16.dp)
        .fillMaxSize()
        .clickable(indication = null, interactionSource = interactionSource) {
            toggled = !toggled
        }
) {
    val offsetTarget = if (toggled) {
        IntOffset(150, 150)
    } else {
        IntOffset.Zero
    }
    val offset = animateIntOffsetAsState(
        targetValue = offsetTarget, label = "offset"
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
    Box(
        modifier = Modifier
            .layout { measurable, constraints ->
                val offsetValue = if (isLookingAhead) offsetTarget else offset.value
                val placeable = measurable.measure(constraints)
                layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) {
                    placeable.placeRelative(offsetValue)
                }
            }
            .size(100.dp)
            .background(colorGreen)
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
}

مربّعان، يتحرّك المربّع الثاني في الموضعَين X وY، ويستجيب المربّع الثالث من خلال تحريك نفسه بالمقدار Y أيضًا.
الشكل 6. إنشاء صور متحركة باستخدام Modifier.layout{ }

تحريك حشو دالة مركّبة

دالة مركّبة خضراء تصبح أصغر وأكبر عند النقر عليها، مع تحريك المساحة المتروكة
الشكل 7. دالة مركّبة مع تحريك الحشو

لتحريك حشو دالة مركّبة، استخدِم animateDpAsState مع Modifier.padding():

var toggled by remember {
    mutableStateOf(false)
}
val animatedPadding by animateDpAsState(
    if (toggled) {
        0.dp
    } else {
        20.dp
    },
    label = "padding"
)
Box(
    modifier = Modifier
        .aspectRatio(1f)
        .fillMaxSize()
        .padding(animatedPadding)
        .background(Color(0xff53D9A1))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            toggled = !toggled
        }
)

إضافة تأثيرات حركية إلى ارتفاع دالة مركّبة

الشكل 8. ارتفاع الدالة المركّبة يتحرك عند النقر

لتحريك ارتفاع دالة مركّبة، استخدِم animateDpAsState مع Modifier.graphicsLayer{ }. لإجراء تغييرات في الارتفاع لمرة واحدة، استخدِم Modifier.shadow(). إذا كنت تريد تحريك الظل، يُعدّ استخدام المُعدِّل Modifier.graphicsLayer{ } الخيار الأفضل من حيث الأداء.

val mutableInteractionSource = remember {
    MutableInteractionSource()
}
val pressed = mutableInteractionSource.collectIsPressedAsState()
val elevation = animateDpAsState(
    targetValue = if (pressed.value) {
        32.dp
    } else {
        8.dp
    },
    label = "elevation"
)
Box(
    modifier = Modifier
        .size(100.dp)
        .align(Alignment.Center)
        .graphicsLayer {
            this.shadowElevation = elevation.value.toPx()
        }
        .clickable(interactionSource = mutableInteractionSource, indication = null) {
        }
        .background(colorGreen)
) {
}

يمكنك بدلاً من ذلك استخدام الدالة المركّبة Card وضبط سمة الارتفاع على قيم مختلفة لكل حالة.

تحريك حجم النص أو ترجمته أو تدويره

دالة مركّبة للنص تعرض كلمة "مرحبًا" وتتحرك بين حجم صغير وحجم أكبر
الشكل 9: نص يتحرك بسلاسة بين حجمَين

عند تحريك حجم النص أو ترجمته أو تدويره، اضبط المَعلمة textMotion في TextStyle على TextMotion.Animated. يضمن ذلك انتقالات أكثر سلاسة بين الرسوم المتحركة النصية. استخدِم Modifier.graphicsLayer{ } لترجمة النص أو تدويره أو تغيير حجمه.

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated)
    )
}

تحريك لون النص

عبارة Hello Compose مع تأثيرات حركية لتغيير لونها بين الأخضر والأزرق
الشكل 10: مثال يعرض لون النص المتحرك

لتحريك لون النص، استخدِم دالة lambda‏ color في العنصر المركّب BasicText:

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val animatedColor by infiniteTransition.animateColor(
    initialValue = Color(0xFF60DDAD),
    targetValue = Color(0xFF4285F4),
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "color"
)

BasicText(
    text = "Hello Compose",
    color = {
        animatedColor
    },
    // ...
)

التبديل بين أنواع مختلفة من المحتوى

شاشة خضراء تعرض كلمة "جارٍ التحميل"، وشاشة زرقاء تعرض كلمة "تم التحميل"، وشاشة بيضاء تعرض كلمة "خطأ"، ويتم التبديل بين الدوال المركّبة المختلفة باستخدام صورة متحركة بسيطة.
الشكل 11: استخدام AnimatedContent لتحريك التغييرات بين الدوال المركّبة المختلفة (تم إبطاؤها)

استخدِم AnimatedContent لإضافة تأثيرات حركية بين الدوال المركّبة المختلفة، وإذا أردت استخدام تأثير التلاشي العادي بين الدوال المركّبة، استخدِم Crossfade.

var state by remember {
    mutableStateOf(UiState.Loading)
}
AnimatedContent(
    state,
    transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
    modifier = Modifier.clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null
    ) {
        state = when (state) {
            UiState.Loading -> UiState.Loaded
            UiState.Loaded -> UiState.Error
            UiState.Error -> UiState.Loading
        }
    },
    label = "Animated Content"
) { targetState ->
    when (targetState) {
        UiState.Loading -> {
            LoadingScreen()
        }
        UiState.Loaded -> {
            LoadedScreen()
        }
        UiState.Error -> {
            ErrorScreen()
        }
    }
}

يمكن تخصيص AnimatedContent لعرض العديد من الأنواع المختلفة من انتقالات الدخول والخروج. لمزيد من المعلومات، يمكنك قراءة المستندات حول AnimatedContent أو قراءة منشور المدوّنة هذا حول AnimatedContent.

استخدام الصور المتحركة أثناء التنقّل إلى وجهات مختلفة

دالتان مركّبتان، إحداهما خضراء مكتوب عليها "الصفحة المقصودة" والأخرى زرقاء مكتوب عليها "التفاصيل"، وتتحركان من خلال تمرير الدالة المركّبة "التفاصيل" فوق الدالة المركّبة "الصفحة المقصودة".
الشكل 12: إنشاء صور متحركة بين الدوال المركّبة باستخدام navigation-compose

لإضافة تأثيرات حركية إلى الانتقالات بين الدوال المركّبة عند استخدام العنصر navigation-compose، حدِّد enterTransition وexitTransition في دالة مركّبة. يمكنك أيضًا ضبط الحركة التلقائية لاستخدامها في جميع الوجهات في المستوى الأعلى NavHost:

val navController = rememberNavController()
NavHost(
    navController = navController, startDestination = "landing",
    enterTransition = { EnterTransition.None },
    exitTransition = { ExitTransition.None }
) {
    composable("landing") {
        ScreenLanding(
            // ...
        )
    }
    composable(
        "detail/{photoUrl}",
        arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }),
        enterTransition = {
            fadeIn(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideIntoContainer(
                animationSpec = tween(300, easing = EaseIn),
                towards = AnimatedContentTransitionScope.SlideDirection.Start
            )
        },
        exitTransition = {
            fadeOut(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideOutOfContainer(
                animationSpec = tween(300, easing = EaseOut),
                towards = AnimatedContentTransitionScope.SlideDirection.End
            )
        }
    ) { backStackEntry ->
        ScreenDetails(
            // ...
        )
    }
}

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

تكرار صورة متحركة

خلفية خضراء تتحول إلى خلفية زرقاء، بشكل لا نهائي من خلال الانتقال بين اللونين.
الشكل 13: لون الخلفية يتحرك بين قيمتين بشكل لا نهائي

استخدِم rememberInfiniteTransition مع infiniteRepeatable animationSpec لتكرار الحركة باستمرار. غيِّر RepeatModes لتحديد كيفية الانتقال ذهابًا وإيابًا.

استخدِم repeatable لتكرار عدد محدّد من المرات.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Green,
    targetValue = Color.Blue,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(color)
    }
) {
    // your composable here
}

بدء صورة متحركة عند تشغيل دالة مركّبة

يتم تشغيل LaunchedEffect عندما يدخل عنصر مركّب إلى التركيب. تبدأ هذه الدالة حركة عند تشغيل دالة مركّبة، ويمكنك استخدامها لتغيير حالة الحركة. استخدام Animatable مع طريقة animateTo لبدء الحركة عند التشغيل:

val alphaAnimation = remember {
    Animatable(0f)
}
LaunchedEffect(Unit) {
    alphaAnimation.animateTo(1f)
}
Box(
    modifier = Modifier.graphicsLayer {
        alpha = alphaAnimation.value
    }
)

إنشاء صور متحركة متسلسلة

أربع دوائر مع أسهم خضراء متحركة بين كل منها، تتحرك واحدة تلو الأخرى.
الشكل 14. مخطّط بياني يوضّح كيفية تقدّم الرسوم المتحركة المتسلسلة، واحدة تلو الأخرى.

استخدِم واجهات برمجة التطبيقات Animatable coroutine لتنفيذ صور متحركة متسلسلة أو متزامنة. يؤدي استدعاء animateTo على Animatable واحدًا تلو الآخر إلى انتظار كل حركة حتى تنتهي الحركات السابقة قبل المتابعة. وذلك لأنّها دالة تعليق.

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    alphaAnimation.animateTo(1f)
    yAnimation.animateTo(100f)
    yAnimation.animateTo(500f, animationSpec = tween(100))
}

إنشاء صور متحركة متزامنة

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

استخدِم واجهات برمجة التطبيقات الخاصة بالروتين المشترك (Animatable#animateTo() أو animate) أو واجهة برمجة التطبيقات Transition لإنشاء صور متحركة متزامنة. إذا كنت تستخدم عدة دوال تشغيل في سياق الكوروتين، فسيتم تشغيل الرسوم المتحركة في نفس الوقت:

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    launch {
        alphaAnimation.animateTo(1f)
    }
    launch {
        yAnimation.animateTo(100f)
    }
}

يمكنك استخدام واجهة برمجة التطبيقات updateTransition لاستخدام الحالة نفسها لتشغيل العديد من الصور المتحركة المختلفة للسمات في الوقت نفسه. يوضّح المثال التالي كيفية تحريك خاصيتين يتم التحكّم فيهما من خلال تغيير الحالة، وهما rect وborderWidth:

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "transition")

val rect by transition.animateRect(label = "rect") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "borderWidth") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

تحسين أداء الصور المتحركة

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

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

لضمان أن يؤدي تطبيقك أقل قدر ممكن من المهام أثناء عرض الصور المتحركة، اختَر إصدار lambda من Modifier حيثما أمكن. يؤدي ذلك إلى تخطّي إعادة الإنشاء وتنفيذ الحركة خارج مرحلة الإنشاء، وإلا يمكنك استخدام Modifier.graphicsLayer{ }، لأنّ هذا المُعدِّل يعمل دائمًا في مرحلة الرسم. لمزيد من المعلومات حول هذا الموضوع، يُرجى الاطّلاع على قسم تأجيل عمليات القراءة في مستندات الأداء.

تغيير توقيت الصور المتحركة

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

في ما يلي ملخّص لخيارات animationSpec المختلفة:

  • ‫spring: صور متحركة مستنِدة إلى الفيزياء، وهي الإعداد التلقائي لجميع الصور المتحركة. يمكنك تغيير الصلابة أو نسبة التخميد للحصول على مظهر وأسلوب مختلفَين للحركة.
  • ‫tween (اختصار لـ between): هي صور متحركة مستندة إلى المدة، وتتحرك بين قيمتَين باستخدام دالة Easing.
  • ‫keyframes: مواصفات لتحديد القيم في نقاط رئيسية معيّنة في صورة متحركة
  • repeatable: مواصفات مستندة إلى المدة يتم تشغيلها لعدد معيّن من المرات، يتم تحديده من خلال RepeatMode.
  • ‫infiniteRepeatable: مواصفات مستندة إلى المدة يتم تشغيلها إلى الأبد.
  • ‫snap: ينتقل زر التحكّم على الفور إلى القيمة النهائية بدون أي حركة.
صورتان متحركتان توضحان عدم ضبط مواصفات مقابل ضبط مواصفات Spring مخصصة.
الشكل 16: مجموعة مواصفات غير محدّدة مقابل مجموعة مواصفات Spring مخصّصة

لمزيد من المعلومات حول animationSpecs، يُرجى قراءة المستندات الكاملة.

مراجع إضافية

لمزيد من الأمثلة على الصور المتحركة الممتعة في Compose، يمكنك الاطّلاع على ما يلي: