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

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

تحريك الخصائص الشائعة للعناصر المركّبة

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

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

عنصر قابل للإنشاء باللون الأخضر يظهر ويختفي
الشكل 1. تحريك ظهور عنصر واختفائه في `Column`

استخدِم 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 واضبط سمة الارتفاع على قيم مختلفة لكل حالة.

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

عنصر قابل للإنشاء يعرض النص Hello مع حركة بين حجم صغير وحجم أكبر
الشكل 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.

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

دالتان مركّبتان، إحداهما خضراء مكتوب عليها Landing والأخرى زرقاء مكتوب عليها Detail، ويتم تحريكهما من خلال تمرير الدالة المركّبة الخاصة بالتفاصيل فوق الدالة المركّبة الخاصة بالصفحة المقصودة.
الشكل 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 لتنفيذ صور متحركة متسلسلة أو متزامنة. يؤدي استدعاء 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: صورة متحركة مستندة إلى الفيزياء، وهي الإعداد التلقائي لجميع الصور المتحركة. يمكنك تغيير الصلابة أو `dampingRatio` لتحقيق مظهر وإحساس مختلفَين بالصورة المتحركة.
  • tween (اختصار between): صورة متحركة مستندة إلى المدة، تتحرّك بين قيمتَين باستخدام دالة Easing.
  • keyframes: مواصفات لتحديد القيم في نقاط رئيسية معيّنة في صورة متحركة.
  • repeatable: مواصفات مستندة إلى المدة يتم تشغيلها عددًا معيّنًا من المرات، محدّدًا من خلال RepeatMode.
  • infiniteRepeatable: مواصفات مستندة إلى المدة يتم تشغيلها بلا حدود.
  • snap: تنتقل على الفور إلى القيمة النهائية بدون أي صورة متحركة.
صورتان متحركتان توضّحان عدم ضبط مواصفات مقابل ضبط مواصفات مخصّصة لـ Spring.
الشكل 16. لم يتم ضبط أي مواصفات مقابل ضبط مواصفات نابضة مخصّصة

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

مراجع إضافية

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