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

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

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

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

به‌طور پیش‌فرض، 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، 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)
)

Low-level animation APIs

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

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

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

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

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

‫Animatable: پویانمایی تک‌مقداری مبتنی بر Coroutine

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

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

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