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

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

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

عنصر قابل للإنشاء يعرض النص 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، يُرجى الاطّلاع على ما يلي: