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

تحريك قيمة واحدة باستخدام 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. تسمح دوال الإضافات هذه بتطبيق جميع الصور المتحركة in/exit/sizeTransform التي كانت من الممكن أن تكون داخلية في AnimatedVisibility/AnimatedContent ليتم نقلها إلى Transition. باستخدام وظائف الإضافة هذه، يمكن ملاحظة تغيير حالة AnimatedVisibility/AnimatedContent من الخارج. بدلاً من المَعلمة المنطقية visible، يأخذ هذا الإصدار من AnimatedVisibility دالة لامبدا تحوّل الحالة المستهدَفة لانتقال العنصر الرئيسي إلى قيمة منطقية.

راجع Animated visibility و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 وتتقلّل بمرور الوقت.