الصور المتحركة المستندة إلى القيمة

إضافة تأثيرات متحركة لقيمة واحدة باستخدام animate*AsState

دوال animate*AsState هي أبسط واجهات برمجة التطبيقات للرسوم المتحركة في أداة "الإنشاء" لتحريك قيمة واحدة. ما عليك سوى تقديم القيمة المستهدَفة (أو القيمة النهائية)، وتبدأ واجهة برمجة التطبيقات عرض الصورة المتحركة من القيمة الحالية إلى القيمة المحدّدة.

في ما يلي مثال على إضافة تأثير متحرك إلى الصورة الشفافة باستخدام واجهة برمجة التطبيقات هذه. من خلال تضمين القيمة المستهدَفة في animateFloatAsState، تصبح قيمة ألفا الآن قيمة متحركة بين القيم المقدَّمة (1f أو 0.5f في هذه الحالة).

var enabled by remember { mutableStateOf(true) }

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")
Box(
    Modifier
        .fillMaxSize()
        .graphicsLayer(alpha = alpha)
        .background(Color.Red)
)

يُرجى العِلم أنّه ليس عليك إنشاء مثيل لأي فئة من فئات الصور المتحركة أو معالجة المقاطعة. في الخلفية، سيتم إنشاء عنصر صورة متحركة (أي مثيل Animatable ) وتذكره في موقع الاستدعاء، مع استخدام قيمة الوجهة الأولى كقيمة أولية. من الآن فصاعدًا، في أيّ وقت تقدّم فيه عنصرًا قابلاً للتجميع بقيمة مستهدفة مختلفة، يبدأ عرض صورة متحركة تلقائيًا نحو تلك القيمة. إذا كان هناك صورة متحركة قيد التنفيذ، تبدأ الصورة المتحركة من قيمتها الحالية (وسرعتها) وتتحرك نحو القيمة المستهدَفة. أثناء التأثير المتحرك، تتم إعادة إنشاء هذا المكوّن القابل للتجميع ويعرض قيمة مُعدَّلة للتأثير المتحرك في كل لقطة.

توفّر ميزة "الإنشاء" تلقائيًا دوال animate*AsState لأنواع البيانات Float، Color، Dp، Size، Offset، Rect، Int، IntOffset، IntSize. يمكنك بسهولة إضافة إمكانية استخدام أنواع بيانات أخرى من خلال توفير TwoWayConverter إلى animateValueAsState يقبل نوعًا عامًا.

يمكنك تخصيص مواصفات الحركة من خلال تقديم AnimationSpec. يمكنك الاطّلاع على AnimationSpec للحصول على مزيد من المعلومات.

إضافة مؤثرات متحركة إلى عناصر متعدّدة في الوقت نفسه باستخدام انتقال

يدير Transition صورة متحركة واحدة أو أكثر كعناصر ثانوية له ويشغّلها بالتزامن بين حالات متعددة.

يمكن أن تكون الحالات من أي نوع بيانات. في العديد من الحالات، يمكنك استخدام enum نوع مخصّص لضمان أمان النوع، كما هو موضّح في هذا المثال:

enum class BoxState {
    Collapsed,
    Expanded
}

ينشئ updateTransition مثيلًا من Transition ويحفظه ويُعدِّل حالته.

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

يمكنك بعد ذلك استخدام إحدى دوالّ التوسيع animate* لتحديد ملف متحركة فرعي في هذا الانتقال. حدِّد القيم المستهدَفة لكل حالة. تعرض دوال animate* هذه قيمة للصورة المتحركة يتم تعديلها في كل إطار أثناء عرض الصورة المتحركة عند تعديل حالة الانتقال باستخدام updateTransition.

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

يمكنك اختياريًا تمرير مَعلمة transitionSpec لتحديد AnimationSpec مختلف لكلّ من مجموعات تغييرات حالة النقل. يمكنك الاطّلاع على AnimationSpec للحصول على مزيد من المعلومات.

val color by transition.animateColor(
    transitionSpec = {
        when {
            BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                spring(stiffness = 50f)

            else ->
                tween(durationMillis = 500)
        }
    }, label = "color"
) { state ->
    when (state) {
        BoxState.Collapsed -> MaterialTheme.colorScheme.primary
        BoxState.Expanded -> MaterialTheme.colorScheme.background
    }
}

بعد وصول عملية النقل إلى الحالة المستهدَفة، ستكون Transition.currentState مساوية Transition.targetState. ويمكن استخدام ذلك كإشارة لتحديد ما إذا كانت عملية النقل قد اكتملت.

نريد أحيانًا الحصول على حالة أولية مختلفة عن الحالة المستهدفة الأولى. يمكننا استخدام updateTransition مع MutableTransitionState لتحقيق هذا. على سبيل المثال، يسمح لنا ببدء عرض الصور المتحركة فور دخول الرمز إلى المزيج.

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = rememberTransition(currentState, label = "box state")
// ……

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

enum class DialerState { DialerMinimized, NumberPad }

@Composable
fun DialerButton(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun NumberPad(isVisibleTransition: Transition<Boolean>) {
    // `isVisibleTransition` spares the need for the content to know
    // about other DialerStates. Instead, the content can focus on
    // animating the state change between visible and not visible.
}

@Composable
fun Dialer(dialerState: DialerState) {
    val transition = updateTransition(dialerState, label = "dialer state")
    Box {
        // Creates separate child transitions of Boolean type for NumberPad
        // and DialerButton for any content animation between visible and
        // not visible
        NumberPad(
            transition.createChildTransition {
                it == DialerState.NumberPad
            }
        )
        DialerButton(
            transition.createChildTransition {
                it == DialerState.DialerMinimized
            }
        )
    }
}

استخدام الانتقال مع AnimatedVisibility وAnimatedContent

تتوفّر AnimatedVisibility وAnimatedContent كدوالّ إضافية لدالة Transition. يتم اشتقاق targetState لملفَّي Transition.AnimatedVisibility وTransition.AnimatedContent من Transition، وبدء عمليات النقل من/إلى الشاشة حسب الحاجة عند تغيُّر targetState لملفَّي Transition. تتيح دوالّ الإضافات هذه نقل كلّ الرسومات المتحركة للدخول/الخروج/تغيير الحجم التي كانت ستتمّ إضافتها إلى AnimatedVisibility/AnimatedContent إلى Transition. باستخدام وظائف الإضافات هذه، يمكن رصد التغيُّر في حالة AnimatedVisibility/AnimatedContent من الخارج. بدلاً من المَعلمة المنطقية visible، يأخذ هذا الإصدار من AnimatedVisibility دالة لامبدا تحوّل الحالة المستهدَفة لانتقال الرئيسي إلى قيمة منطقية.

اطّلِع على AnimatedVisibility وAnimatedContent للاطّلاع على التفاصيل.

var selected by remember { mutableStateOf(false) }
// Animates changes when `selected` is changed.
val transition = updateTransition(selected, label = "selected state")
val borderColor by transition.animateColor(label = "border color") { isSelected ->
    if (isSelected) Color.Magenta else Color.White
}
val elevation by transition.animateDp(label = "elevation") { isSelected ->
    if (isSelected) 10.dp else 2.dp
}
Surface(
    onClick = { selected = !selected },
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp, borderColor),
    shadowElevation = elevation
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(text = "Hello, world!")
        // AnimatedVisibility as a part of the transition.
        transition.AnimatedVisibility(
            visible = { targetSelected -> targetSelected },
            enter = expandVertically(),
            exit = shrinkVertically()
        ) {
            Text(text = "It is fine today.")
        }
        // AnimatedContent as a part of the transition.
        transition.AnimatedContent { targetState ->
            if (targetState) {
                Text(text = "Selected")
            } else {
                Icon(imageVector = Icons.Default.Phone, contentDescription = "Phone")
            }
        }
    }
}

تجميع انتقال وجعله قابلاً لإعادة الاستخدام

بالنسبة إلى حالات الاستخدام البسيطة، يُعدّ تحديد الصور المتحركة للانتقالات في العنصر القابل للتجميع نفسه لواجهة المستخدم خيارًا صالحًا تمامًا. عند العمل على مكوّن معقّد يتضمّن عددًا من القيم المتحرّكة، قد تحتاج إلى فصل تنفيذ الحركة من واجهة المستخدم القابلة للتجميع.

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

enum class BoxState { Collapsed, Expanded }

@Composable
fun AnimatingBox(boxState: BoxState) {
    val transitionData = updateTransitionData(boxState)
    // UI tree
    Box(
        modifier = Modifier
            .background(transitionData.color)
            .size(transitionData.size)
    )
}

// Holds the animation values.
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    val color by color
    val size by size
}

// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    val transition = updateTransition(boxState, label = "box state")
    val color = transition.animateColor(label = "color") { state ->
        when (state) {
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "size") { state ->
        when (state) {
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) { TransitionData(color, size) }
}

إنشاء صورة متحركة متكرّرة إلى ما لا نهاية باستخدام rememberInfiniteTransition

يحتوي InfiniteTransition على صورة متحركة فرعية واحدة أو أكثر مثل Transition، ولكن تبدأ الصور المتحركة في التشغيل فور دخولها إلى التركيب ولا تتعطل إلا إذا تمت إزالتها. يمكنك إنشاء مثيل من InfiniteTransition باستخدام rememberInfiniteTransition. يمكن إضافة صور متحركة للأطفال باستخدام animateColor أو animatedFloat أو animatedValue. عليك أيضًا تحديد infiniteRepeatable لتحديد مواصفات المؤثر المتحرك.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

واجهات برمجة التطبيقات المخصّصة للصور المتحركة المنخفضة المستوى

تم إنشاء جميع واجهات برمجة التطبيقات ذات المستوى العالي للرسوم المتحركة المذكورة في القسم السابق على أساس واجهات برمجة التطبيقات ذات المستوى المنخفض للرسوم المتحركة.

دوال animate*AsState هي أبسط واجهات برمجة التطبيقات، وهي تعرِض تغييرًا فوريًا في القيمة كقيمة للحركة. وتستند هذه الدالة إلى Animatable، وهي واجهة برمجة تطبيقات مستندة إلى دالّة تناوب المهام لإضافة تأثيرات متحركة إلى قيمة واحدة. updateTransition ينشئ كائن انتقال يمكنه إدارة قيم متعدّدة للحركة وتنفيذها استنادًا إلى تغيير الحالة. يشبه rememberInfiniteTransition، ولكنه ينشئ انتقالًا لانهائيًا يمكنه إدارة حركات متعدّدة تستمر في التشغيل إلى أجل غير مسمى. جميع واجهات برمجة التطبيقات هذه قابلة للتجميع باستثناء Animatable، ما يعني أنّه يمكن إنشاء هذه الصور المتحركة خارج عملية التركيب.

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

رسم بياني يعرض العلاقة بين واجهات برمجة التطبيقات المختلفة للرسوم المتحركة المنخفضة المستوى

Animatable: صورة متحرّكة لقيمة فردية تستند إلى كوروتين

Animatable هو عنصر يُستخدَم لعرض القيمة ويمكنه إضافة تأثيرات متحركة إليها عند تغييرها من خلال animateTo. هذه هي واجهة برمجة التطبيقات التي تدعم تنفيذ animate*AsState. ويضمن ذلك الاستمرارية المتسقة والحصرية المتبادلة، ما يعني أنّ تغيير القيمة يكون مستمرًا دائمًا وسيتم إلغاء أيّ حركة متحركة جارية.

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

// Start out gray and animate to green/red based on `ok`
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Box(
    Modifier
        .fillMaxSize()
        .background(color.value)
)

في المثال أعلاه، ننشئ مثيلًا من Animatable ونتذكره بالقيمة الأولية Color.Gray. استنادًا إلى قيمة العلامة المنطقية ok، يتم عرض اللون بشكل متحرك إما على Color.Green أو Color.Red. يؤدي أي تغيير لاحق في القيمة المنطقية إلى بدء عرض متحرك للون الآخر. إذا كانت هناك صورة متحركة جارية عند تغيير القيمة، يتم إلغاء الصورة المتحركة، ويبدأ العرض الجديد للصورة المتحركة من قيمة اللقطة الحالية بالسرعة الحالية.

هذا هو تطبيق الصور المتحركة الذي يدعم animate*AsState API المذكور في القسم السابق. مقارنةً بـ animate*AsState، يمنحنا استخدام Animatable مباشرةً إمكانية التحكّم بشكل أدق في عدة جوانب. أولاً، يمكن أن يكون لـAnimatable قيمة أولية مختلفة عن قيمته المستهدَفة الأولى. على سبيل المثال، يعرض مثال الرمز البرمجي أعلاه مربّعًا رماديًا في البداية، والذي يبدأ على الفور بالتحرّك إلى اللون الأخضر أو الأحمر. ثانيًا، توفّر Animatable المزيد من العمليات على قيمة المحتوى، وهي snapTo وanimateDecay. snapTo يضبط القيمة الحالية على القيمة المستهدَفة على الفور. يكون ذلك مفيدًا عندما لا يكون المؤثر المتحرك هو المصدر الوحيد للحقيقة ويجب مزامنته مع حالات أخرى، مثل أحداث اللمس. animateDecay يبدأ صورة متحركة تتباطأ من السرعة المحدّدة. ويُعدّ ذلك مفيدًا لتنفيذ سلوك الرمي. اطّلِع على الإيماءات والرسوم المتحركة للحصول على مزيد من المعلومات.

يتيح Animatable تلقائيًا استخدام Float وColor، ولكن يمكن استخدام أي نوع بيانات من خلال توفير TwoWayConverter. اطّلِع على AnimationVector للحصول على مزيد من المعلومات.

يمكنك تخصيص مواصفات الحركة من خلال تقديم AnimationSpec. يمكنك الاطّلاع على AnimationSpec للحصول على مزيد من المعلومات.

Animation: صورة متحرّكة يتم التحكّم فيها يدويًا

Animation هو أدنى مستوى لواجهة برمجة التطبيقات Animation API متاح. إنّ العديد من الصور المتحركة التي رأيناها حتى الآن تستند إلى ميزة "الصور المتحركة". هناك نوعان فرعيان من Animation: TargetBasedAnimation وDecayAnimation.

يجب عدم استخدام Animation إلا للتحكّم يدويًا في وقت عرض الصورة المتحركة. لا يرتبط الترميز Animation بحالة معيّنة، ولا يتضمّن أي مفهوم لدورة الحياة. ويُعدّ محرّكًا لحساب الرسوم المتحركة تستخدمه واجهات برمجة التطبيقات ذات المستوى الأعلى.

TargetBasedAnimation

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

val anim = remember {
    TargetBasedAnimation(
        animationSpec = tween(200),
        typeConverter = Float.VectorConverter,
        initialValue = 200f,
        targetValue = 1000f
    )
}
var playTime by remember { mutableLongStateOf(0L) }

LaunchedEffect(anim) {
    val startTime = withFrameNanos { it }

    do {
        playTime = withFrameNanos { it } - startTime
        val animationValue = anim.getValueFromNanos(playTime)
    } while (someCustomCondition())
}

DecayAnimation

على عكس TargetBasedAnimation، DecayAnimation لا تتطلّب سمة targetValue. بدلاً من ذلك، يتم احتساب targetValue استنادًا إلى شروط البدء التي تم ضبطها بواسطة initialVelocity و initialValue وDecayAnimationSpec المقدَّمة.

غالبًا ما يتم استخدام الرسوم المتحرّكة للانحسار بعد إيماءة التمرير السريع لإبطاء العناصر إلى أن تتوقف. تبدأ سرعة الصورة المتحركة بالقيمة التي تم ضبطها باستخدام initialVelocityVector وتتباطأ بمرور الوقت.