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

تحريك قيمة واحدة باستخدام animate*AsState

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

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

var enabled by remember { mutableStateOf(true) }

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

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

توفّر Compose تلقائيًا دوال 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 تعبير lambda يحوّل حالة الانتقال الرئيسية إلى قيمة منطقية.

يمكنك الاطّلاع على 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")
            }
        }
    }
}

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

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

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

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 composable لإنشاء نطاق لمدة قيمة المفتاح المحدّدة فقط.

// 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 المذكورة في القسم السابق. مقارنةً بـ animate*AsState، يتيح لنا استخدام Animatable مباشرةً التحكّم بشكل أكثر دقة في عدّة جوانب. أولاً، يمكن أن يكون لقيمة Animatable قيمة أولية مختلفة عن قيمة الاستهداف الأولى. على سبيل المثال، يعرض نموذج الرمز البرمجي أعلاه مربّعًا رماديًا في البداية، ثم يبدأ على الفور في عرض حركة إما باللون الأخضر أو الأحمر. ثانيًا، يوفّر Animatable المزيد من العمليات على قيمة المحتوى، وتحديدًا snapTo وanimateDecay. يؤدي snapTo إلى ضبط القيمة الحالية على القيمة المستهدَفة على الفور. ويكون ذلك مفيدًا عندما لا يكون الرسم المتحرّك هو المصدر الوحيد للحقيقة ويجب مزامنته مع حالات أخرى، مثل أحداث اللمس. يبدأ animateDecay صورة متحركة تبطئ من السرعة المحدّدة. ويكون ذلك مفيدًا لتنفيذ سلوك التمرير السريع. يمكنك الاطّلاع على الإيماءات والرسوم المتحركة لمزيد من المعلومات.

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

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

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

Animation هو أدنى مستوى لواجهة برمجة تطبيقات الرسوم المتحركة المتاحة. تستند العديد من الصور المتحركة التي رأيناها حتى الآن إلى Animation. هناك نوعان فرعيان من 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 وتتباطأ بمرور الوقت.