أدوات تعديل الصور المتحركة والتركيبات

تتضمّن Compose عناصر قابلة للإنشاء ومعدِّلات مدمجة للتعامل مع حالات الاستخدام الشائعة للرسوم المتحركة.

عناصر مركّبة متحركة مضمّنة

توفّر Compose العديد من العناصر القابلة للإنشاء التي تحرّك ظهور المحتوى واختفاءه وتغييرات التصميم.

تحريك ظهور العناصر واختفائها

عنصر قابل للإنشاء باللون الأخضر يظهر ويختفي
الشكل 1. تحريك ظهور عنصر واختفائه في عمود

تعمل الدالة المركّبة AnimatedVisibility على تحريك ظهور محتواها واختفائه.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

يظهر المحتوى تلقائيًا من خلال التلاشي تدريجيًا والتوسّع، ويختفي من خلال التلاشي تدريجيًا والتصغير. خصِّص هذا الانتقال من خلال تحديد الكائنَين EnterTransition وExitTransition.

var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
AnimatedVisibility(
    visible = visible,
    enter = slideInVertically {
        // Slide in from 40 dp from the top.
        with(density) { -40.dp.roundToPx() }
    } + expandVertically(
        // Expand from the top.
        expandFrom = Alignment.Top
    ) + fadeIn(
        // Fade in with the initial alpha of 0.3f.
        initialAlpha = 0.3f
    ),
    exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
    Text(
        "Hello",
        Modifier
            .fillMaxWidth()
            .height(200.dp)
    )
}

كما هو موضّح في المثال السابق، يمكنك دمج عدة عناصر EnterTransition أو ExitTransition باستخدام عامل التشغيل +، ويقبل كل عنصر مَعلمات اختيارية لتخصيص سلوكه. يمكنك الاطّلاع على صفحات المراجع للحصول على مزيد من المعلومات.

أمثلة على تأثيرات الانتقال للدخول والخروج

EnterTransition ExitTransition
fadeIn
يظهر عنصر في واجهة المستخدم تدريجيًا.
fadeOut
يتلاشى عنصر في واجهة المستخدم تدريجيًا من العرض.
slideIn
يظهر عنصر في واجهة المستخدم على الشاشة من خارجها.
slideOut
ينزلق عنصر واجهة المستخدم خارج الشاشة.
slideInHorizontally
ينزلق عنصر في واجهة المستخدم أفقيًا إلى العرض.
slideOutHorizontally
ينزلق عنصر في واجهة المستخدم أفقيًا خارج نطاق العرض.
slideInVertically
ينزلق عنصر في واجهة المستخدم عموديًا إلى داخل الشاشة.
slideOutVertically
ينزلق عنصر في واجهة المستخدم عموديًا خارج نطاق العرض.
scaleIn
يتم تكبير عنصر في واجهة المستخدم وإظهاره.
scaleOut
يتم تصغير عنصر في واجهة المستخدم وإخفاؤه.
expandIn
يتوسّع عنصر في واجهة المستخدم ليظهر من نقطة مركزية.
shrinkOut
يتقلّص عنصر في واجهة المستخدم خارج نطاق العرض إلى نقطة مركزية.
expandHorizontally
يتوسّع عنصر في واجهة المستخدم أفقيًا ليظهر.
shrinkHorizontally
يتقلّص عنصر في واجهة المستخدم أفقيًا خارج نطاق العرض.
expandVertically
يتوسّع أحد عناصر واجهة المستخدم عموديًا ليصبح مرئيًا.
shrinkVertically
يتقلّص عنصر في واجهة المستخدم عموديًا خارج نطاق العرض.

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

// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
    MutableTransitionState(false).apply {
        // Start the animation immediately.
        targetState = true
    }
}
Column {
    AnimatedVisibility(visibleState = state) {
        Text(text = "Hello, world!")
    }

    // Use the MutableTransitionState to know the current animation state
    // of the AnimatedVisibility.
    Text(
        text = when {
            state.isIdle && state.currentState -> "Visible"
            !state.isIdle && state.currentState -> "Disappearing"
            state.isIdle && !state.currentState -> "Invisible"
            else -> "Appearing"
        }
    )
}

تحريك الدخول والخروج للأطفال

يمكن للمحتوى ضِمن AnimatedVisibility (العناصر الفرعية المباشرة أو غير المباشرة) استخدام المعدِّل animateEnterExit لتحديد سلوك مختلف للرسوم المتحركة لكل عنصر منها. التأثير المرئي لكل عنصر من هذه العناصر الفرعية هو مزيج من الرسوم المتحركة المحددة في العنصر القابل للإنشاء AnimatedVisibility والرسوم المتحركة الخاصة بالعنصر الفرعي عند الدخول والخروج.

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) {
    // Fade in/out the background and the foreground.
    Box(
        Modifier
            .fillMaxSize()
            .background(Color.DarkGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .animateEnterExit(
                    // Slide in/out the inner box.
                    enter = slideInVertically(),
                    exit = slideOutVertically()
                )
                .sizeIn(minWidth = 256.dp, minHeight = 64.dp)
                .background(Color.Red)
        ) {
            // Content of the notification…
        }
    }
}

في بعض الحالات، قد تريد أن يطبّق AnimatedVisibility عدم ظهور أي رسوم متحركة على الإطلاق، كي يتمكّن الأطفال من الحصول على رسوم متحركة مميزة خاصة بهم من خلال animateEnterExit. لتحقيق ذلك، حدِّد EnterTransition.None وExitTransition.None في الدالة المركّبة AnimatedVisibility.

إضافة صورة متحركة مخصّصة

إذا أردت إضافة تأثيرات حركة مخصّصة تتجاوز الحركات المضمّنة للدخول والخروج، يمكنك الوصول إلى مثيل Transition الأساسي باستخدام السمة transition داخل تعبير lambda الخاص بالمحتوى لـ AnimatedVisibility. سيتم تشغيل أي حالات حركة تمت إضافتها إلى مثيل Transition في الوقت نفسه مع حركات الدخول والخروج في AnimatedVisibility. تنتظر AnimatedVisibility إلى أن تنتهي جميع الصور المتحركة في Transition قبل إزالة محتواها. بالنسبة إلى رسوم الخروج المتحركة التي تم إنشاؤها بشكل مستقل عن Transition (مثل استخدام animate*AsState)، لن يتمكّن AnimatedVisibility من احتسابها، وبالتالي قد يزيل العنصر القابل للإنشاء قبل اكتمالها.

var visible by remember { mutableStateOf(true) }

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) { // this: AnimatedVisibilityScope
    // Use AnimatedVisibilityScope#transition to add a custom animation
    // to the AnimatedVisibility.
    val background by transition.animateColor(label = "color") { state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Gray
    }
    Box(
        modifier = Modifier
            .size(128.dp)
            .background(background)
    )
}

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

التحريك استنادًا إلى الحالة المستهدَفة

تعمل AnimatedContent الدالة المركّبة على تحريك محتواها أثناء تغيُّره استنادًا إلى حالة مستهدَفة.

Row {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Add")
    }
    AnimatedContent(
        targetState = count,
        label = "animated content"
    ) { targetCount ->
        // Make sure to use `targetCount`, not `count`.
        Text(text = "Count: $targetCount")
    }
}

يتم تلقائيًا إخفاء المحتوى الأولي تدريجيًا ثم إظهار المحتوى المستهدف تدريجيًا (يُطلق على هذا السلوك اسم التلاشي التدريجي). يمكنك تخصيص سلوك هذه الحركة من خلال تحديد عنصر ContentTransform للمَعلمة transitionSpec. يمكنك إنشاء مثيل من ContentTransform من خلال دمج عنصر EnterTransition مع عنصر ExitTransition باستخدام الدالة with ذات التدوين الداخلي. يمكنك تطبيق SizeTransform على العنصر ContentTransform من خلال إرفاقه بالدالة using ذات الصيغة الداخلية.

AnimatedContent(
    targetState = count,
    transitionSpec = {
        // Compare the incoming number with the previous number.
        if (targetState > initialState) {
            // If the target number is larger, it slides up and fades in
            // while the initial (smaller) number slides up and fades out.
            slideInVertically { height -> height } + fadeIn() togetherWith
                slideOutVertically { height -> -height } + fadeOut()
        } else {
            // If the target number is smaller, it slides down and fades in
            // while the initial number slides down and fades out.
            slideInVertically { height -> -height } + fadeIn() togetherWith
                slideOutVertically { height -> height } + fadeOut()
        }.using(
            // Disable clipping since the faded slide-in/out should
            // be displayed out of bounds.
            SizeTransform(clip = false)
        )
    }, label = "animated content"
) { targetCount ->
    Text(text = "$targetCount")
}

تحدّد EnterTransition طريقة ظهور المحتوى المستهدَف، بينما تحدّد ExitTransition طريقة اختفاء المحتوى الأوّلي. بالإضافة إلى جميع دوال EnterTransition وExitTransition المتاحة في AnimatedVisibility، يوفّر AnimatedContent slideIntoContainer وslideOutOfContainer. هذه بدائل مناسبة للسمتَين slideInHorizontally/Vertically وslideOutHorizontally/Vertically اللتين تحتسبان مسافة الانزلاق استنادًا إلى أحجام المحتوى الأولي والمحتوى المستهدف في عنصر AnimatedContent.

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

var expanded by remember { mutableStateOf(false) }
Surface(
    color = MaterialTheme.colorScheme.primary,
    onClick = { expanded = !expanded }
) {
    AnimatedContent(
        targetState = expanded,
        transitionSpec = {
            fadeIn(animationSpec = tween(150, 150)) togetherWith
                fadeOut(animationSpec = tween(150)) using
                SizeTransform { initialSize, targetSize ->
                    if (targetState) {
                        keyframes {
                            // Expand horizontally first.
                            IntSize(targetSize.width, initialSize.height) at 150
                            durationMillis = 300
                        }
                    } else {
                        keyframes {
                            // Shrink vertically first.
                            IntSize(initialSize.width, targetSize.height) at 150
                            durationMillis = 300
                        }
                    }
                }
        }, label = "size transform"
    ) { targetExpanded ->
        if (targetExpanded) {
            Expanded()
        } else {
            ContentIcon()
        }
    }
}

تحريك انتقالات دخول وخروج العناصر الفرعية

تمامًا مثل AnimatedVisibility، يتوفّر المعدِّل animateEnterExit داخل دالة lambda الخاصة بالمحتوى في AnimatedContent. استخدِم هذه السمة لتطبيق EnterAnimation وExitAnimation على كل من العناصر الفرعية المباشرة أو غير المباشرة بشكل منفصل.

إضافة صورة متحركة مخصّصة

تمامًا مثل AnimatedVisibility، يتوفّر الحقل transition داخل دالة lambda الخاصة بالمحتوى في AnimatedContent. استخدِم هذه السمة لإنشاء تأثير رسوم متحركة مخصّص يعمل في الوقت نفسه مع الانتقال AnimatedContent. يمكنك الاطّلاع على updateTransition لمعرفة التفاصيل.

التبديل بين تخطيطَين

Crossfade يتم تحريكه بين تخطيطَين باستخدام تأثير التلاشي والظهور. من خلال تبديل القيمة التي يتم تمريرها إلى المَعلمة current، يتم تبديل المحتوى باستخدام تأثير تلاشي وتداخل.

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage, label = "cross fade") { screen ->
    when (screen) {
        "A" -> Text("Page A")
        "B" -> Text("Page B")
    }
}

معدِّلات الصور المتحركة المضمّنة

توفّر Compose معدِّلات لتحريك تغييرات معيّنة مباشرةً على العناصر القابلة للإنشاء.

تحريك تغييرات حجم العناصر القابلة للإنشاء

عنصر قابل للإنشاء باللون الأخضر يحرّك تغيير حجمه بسلاسة
الشكل 2. عنصر قابل للإنشاء يمكن تحريكه بسلاسة بين حجم صغير وحجم أكبر

يُنشئ المعدِّل animateContentSize رسمًا متحركًا لتغيير الحجم.

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

الصور المتحركة لعناصر القائمة

إذا كنت تريد تحريك عمليات إعادة ترتيب العناصر داخل قائمة أو شبكة Lazy، يمكنك الاطّلاع على مستندات حول تحريك عناصر التنسيق Lazy.