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

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

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

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

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

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

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

Animatable API هو التنفيذ الأساسي لـ 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 وتتباطأ بمرور الوقت.