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

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

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

عبارة قابلة للإنشاء من النص
الشكل 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)
    )
}

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

الكلمات
الشكل 10. مثال يوضّح تحريك لون النص

لتحريك لون النص، استخدِم دالة لامدا 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 لتنفيذ صور متحركة متسلسلة أو متزامنة. يؤدي استدعاء 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: التركيب والتنسيق والرسم. إذا كانت الصورة المتحركة تغيّر مرحلة التنسيق، فإنّها تتطلب إعادة تنسيق جميع العناصر المركّبة المتأثرة وإعادة رسمها. إذا كانت الصورة المتحركة تحدث في مرحلة الرسم، فإنّها تكون تلقائيًا أكثر فعالية من حيث الأداء مقارنةً بتشغيل الصورة المتحركة في مرحلة التنسيق، لأنّها ستتطلب جهدًا أقل بشكل عام.

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

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

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

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

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

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

مراجع إضافية

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