پویانمایی‌های ارزش‌محور

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

پویانمایی مقدار تکی با animate*AsState

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

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

به‌طور پیش‌فرض، «نگارش» 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 تغییر می‌کند، پویانمایی‌های ورود، خروج، و sizeTransform را درصورت نیاز راه‌اندازی می‌کند. این توابع افزونه به شما امکان می‌دهند همه پویانمایی‌های ورود، خروج، و 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، 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، به شخصی‌سازی پویانمایی‌ها مراجعه کنید.

ارتباط بین میاناهای برنامه‌سازی کاربردی پویانمایی سطح پایین
شکل ۱. ارتباط بین میاناهای برنامه‌سازی کاربردی پویانمایی سطح پایین.

‫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 تنظیم می‌کند شروع می‌شود و به‌مرور زمان کاهش می‌یابد.