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

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

تحريك قيمة واحدة باستخدام 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) وحفظه في موقع الاستدعاء، مع استخدام قيمة الهدف الأولى كقيمة أولية. ومنذ ذلك الحين، في أي وقت تقدّم فيه لهذه الدالة المركّبة قيمة مستهدفة مختلفة، يتم بدء حركة تلقائيًا نحو هذه القيمة. إذا كانت هناك حركة قيد التشغيل، تبدأ الحركة من قيمتها الحالية (وسرعتها) وتتحرك نحو القيمة المستهدفة. أثناء الحركة، تتم إعادة تركيب هذه الدالة المركّبة وتعرض قيمة حركة معدَّلة لكل إطار.

توفّر أداة "الإنشاء" تلقائيًا دوال 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، ويتم تفعيل حركات الدخول والخروج وsizeTransform حسب الحاجة عند تغيير targetState في Transition. تتيح لك دوال الإضافة هذه رفع جميع صور الدخول والخروج وsizeTransform المتحركة التي قد تكون داخلية في 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")
            }
        }
    }
}

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

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

يمكنك إجراء ذلك من خلال إنشاء فئة تحتوي على جميع قيم الصور المتحركة ودالة 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 أو animateFloat أو animateValue. يجب أيضًا تحديد 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، يمكنك الوصول إلى بعض إمكانات التخصيص من خلال واجهات برمجة تطبيقات ذات مستوى أعلى. يمكنك الاطّلاع على تخصيص الصور المتحركة لمزيد من المعلومات حول AnimationVector وAnimationSpec.

العلاقة بين واجهات برمجة التطبيقات للصور المتحركة المنخفضة المستوى
الشكل 1. العلاقة بين واجهات برمجة التطبيقات Animation API المنخفضة المستوى

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

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

العديد من ميزات Animatable، بما في ذلك animateTo، هي دوال تعليق. وهذا يعني أنّه يجب تضمينها في نطاق دالة coroutine مناسب. على سبيل المثال، يمكنك استخدام الدالة المركّبة 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. ويؤدي أي تغيير لاحق في القيمة المنطقية إلى بدء حركة إلى اللون الآخر. إذا كانت هناك صورة متحركة قيد التشغيل عند تغيُّر القيمة، يلغي Compose الصورة المتحركة، وتبدأ الصورة المتحركة الجديدة من قيمة اللقطة الحالية بالسرعة الحالية.

واجهة برمجة التطبيقات Animatable هذه هي التنفيذ الأساسي لـ animate*AsState المذكورة في القسم السابق. يتيح استخدام Animatable مباشرةً التحكّم بشكل أكثر دقة بعدة طرق:

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

لمزيد من المعلومات، يُرجى الاطّلاع على الإيماءات والحركات.

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

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

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

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