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

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

تحريك الخصائص الشائعة القابلة للإنشاء

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

إنشاء صورة متحرّكة تظهر فيها العناصر وتختفي

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

استخدِم AnimatedVisibility لإخفاء أو إظهار عنصر Composable. يمكن للأطفال الذين تتراوح أعمارهم بين 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)
    )
}

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

الكلمات
الشكل 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 لتحديد كيفية الرجوع والتقدّم.

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

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

يمكنك الاطّلاع على المستندات الكاملة للحصول على مزيد من المعلومات حول animationSpecs.

مراجع إضافية

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