انیمیشن های مبتنی بر ارزش

این صفحه نحوه ایجاد انیمیشن‌های مبتنی بر مقدار در Jetpack Compose را شرح می‌دهد و بر APIهایی تمرکز دارد که مقادیر را بر اساس حالت‌های فعلی و هدفشان متحرک‌سازی می‌کنند.

متحرک‌سازی یک مقدار واحد با animate*AsState

توابع animate*AsState ، APIهای انیمیشن سرراستی در Compose برای متحرک‌سازی یک مقدار واحد هستند. شما فقط مقدار هدف (یا مقدار پایانی) را ارائه می‌دهید و API انیمیشن را از مقدار فعلی تا مقدار مشخص شده شروع می‌کند.

مثال زیر با استفاده از این API، آلفا را متحرک می‌کند. با قرار دادن مقدار هدف در 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 ) ایجاد و در محل فراخوانی به خاطر سپرده می‌شود، با اولین مقدار هدف به عنوان مقدار اولیه آن. از آنجا به بعد، هر زمان که به این composable مقدار هدف متفاوتی بدهید، یک انیمیشن به طور خودکار به سمت آن مقدار شروع می‌شود. اگر از قبل انیمیشنی در حال اجرا باشد، انیمیشن از مقدار فعلی (و سرعت) خود شروع می‌شود و به سمت مقدار هدف حرکت می‌کند. در طول انیمیشن، این composable دوباره ترکیب می‌شود و در هر فریم یک مقدار انیمیشن به‌روز شده را برمی‌گرداند.

به طور پیش‌فرض، 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 مشتق شده است و انیمیشن‌های enter، exit و sizeTransform را در صورت نیاز، زمانی که targetState مربوط به Transition تغییر می‌کند، فعال می‌کند. این توابع افزونه به شما امکان می‌دهند تمام انیمیشن‌های enter، exit و sizeTransform را که در غیر این صورت درون AnimatedVisibility / AnimatedContent قرار می‌گرفتند، به Transition منتقل کنید. با این توابع افزونه، می‌توانید تغییر وضعیت AnimatedVisibility / AnimatedContent را از بیرون مشاهده کنید. به جای یک پارامتر boolean visible ، این نسخه از AnimatedVisibility یک لامبدا می‌گیرد که وضعیت هدف گذار والد را به boolean تبدیل می‌کند.

برای جزئیات بیشتر به 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 را در خود جای می‌دهد، اما انیمیشن‌ها به محض ورود به ترکیب شروع به اجرا می‌کنند و تا زمانی که حذف نشوند، متوقف نمی‌شوند. می‌توانید با rememberInfiniteTransition یک نمونه از InfiniteTransition ایجاد کنید و انیمیشن‌های فرزند را با 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)
)

API های انیمیشن سطح پایین

تمام APIهای انیمیشن سطح بالا که در بخش قبل به آنها اشاره شد، بر اساس APIهای انیمیشن سطح پایین ساخته شده‌اند.

توابع animate*AsState رابط‌های برنامه‌نویسی کاربردی (API) سرراستی هستند که تغییر مقدار فوری را به عنوان یک مقدار انیمیشن رندر می‌کنند. این قابلیت توسط Animatable ، یک API مبتنی بر کوروتین برای متحرک‌سازی یک مقدار واحد، پشتیبانی می‌شود.

updateTransition یک شیء گذار ایجاد می‌کند که می‌تواند چندین مقدار انیمیشن را مدیریت کند و هنگام تغییر حالت، آنها را اجرا کند. rememberInfiniteTransition مشابه است، اما یک گذار بی‌نهایت ایجاد می‌کند که می‌تواند چندین انیمیشن را که به طور نامحدود ادامه می‌یابند، مدیریت کند. همه این APIها به جز Animatable ، قابل ترکیب هستند، به این معنی که می‌توانید این انیمیشن‌ها را خارج از ترکیب ایجاد کنید.

همه این APIها بر اساس API بنیادی‌تر Animation ساخته شده‌اند. اگرچه اکثر برنامه‌ها مستقیماً با Animation تعامل ندارند، اما می‌توانید از طریق APIهای سطح بالاتر به برخی از قابلیت‌های سفارشی‌سازی آن دسترسی داشته باشید. برای اطلاعات بیشتر در مورد AnimationVector و AnimationSpec به بخش Customize animations مراجعه کنید.

رابطه بین API های انیمیشن سطح پایین
شکل ۱. رابطه بین APIهای انیمیشن سطح پایین.

Animatable : انیمیشن تک مقداری مبتنی بر Coroutine

Animatable یک نگهدارنده مقدار است که می‌تواند مقدار را هنگام تغییر با استفاده از animateTo متحرک کند. این API است که از پیاده‌سازی animate*AsState پشتیبانی می‌کند. این API تداوم سازگار و انحصار متقابل را تضمین می‌کند، به این معنی که تغییر مقدار همیشه پیوسته است و Compose هرگونه انیمیشن در حال انجام را لغو می‌کند.

بسیاری از ویژگی‌های Animatable ، از جمله animateTo ، توابع suspend هستند. این بدان معناست که شما باید آنها را در یک محدوده کوروتین مناسب قرار دهید. برای مثال، می‌توانید از 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 انیمیشن را لغو می‌کند و انیمیشن جدید از مقدار snapshot فعلی با سرعت فعلی شروع می‌شود.

این API Animatable ، پیاده‌سازی اساسی برای animate*AsState است که در بخش قبلی به آن اشاره شد. استفاده مستقیم از Animatable کنترل دقیق‌تری را از چندین طریق ارائه می‌دهد:

  • اول اینکه، Animatable می‌تواند مقدار اولیه‌ای متفاوت از اولین مقدار هدف خود داشته باشد. برای مثال، مثال کد قبلی در ابتدا یک جعبه خاکستری را نشان می‌دهد که بلافاصله به رنگ سبز یا قرمز تغییر رنگ می‌دهد.
  • دوم، Animatable عملیات بیشتری روی مقدار محتوا، به ویژه snapTo و animateDecay ، ارائه می‌دهد.
    • snapTo مقدار فعلی را فوراً به مقدار هدف تنظیم می‌کند. این زمانی مفید است که انیمیشن تنها منبع حقیقت نباشد و باید با حالت‌های دیگر، مانند رویدادهای لمسی، همگام‌سازی شود.
    • animateDecay انیمیشنی را شروع می‌کند که از سرعت داده شده کندتر می‌شود. این برای پیاده‌سازی رفتار fling مفید است.

برای اطلاعات بیشتر به بخش ژست و انیمیشن مراجعه کنید.

به طور پیش‌فرض، Animatable Float و Color پشتیبانی می‌کند، اما می‌توانید با ارائه TwoWayConverter از هر نوع داده‌ای استفاده کنید. برای اطلاعات بیشتر به AnimationVector مراجعه کنید.

شما می‌توانید مشخصات انیمیشن را با ارائه AnimationSpec سفارشی کنید. برای اطلاعات بیشتر به AnimationSpec مراجعه کنید.

Animation : انیمیشن کنترل دستی

Animation پایین‌ترین سطح API انیمیشن موجود است. بسیاری از انیمیشن‌هایی که تاکنون دیده‌ایم بر اساس Animation ساخته شده‌اند. دو زیرگروه Animation وجود دارد: TargetBasedAnimation و DecayAnimation .

فقط از Animation برای کنترل دستی زمان انیمیشن استفاده کنید. Animation بدون وضعیت است و هیچ مفهومی از چرخه حیات ندارد. این به عنوان یک موتور محاسبه انیمیشن برای API های سطح بالاتر عمل می کند.

TargetBasedAnimation

سایر APIها اکثر موارد استفاده را پوشش می‌دهند، اما استفاده مستقیم از 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 ارائه شده تنظیم می‌شود، محاسبه می‌کند.

انیمیشن‌های Decay اغلب پس از یک حرکت fling برای کند کردن عناصر تا توقف استفاده می‌شوند. سرعت انیمیشن از مقداری که initialVelocityVector تعیین می‌کند شروع می‌شود و به مرور زمان کندتر می‌شود.

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}