آموزش ساخت انیمیشن در Jetpack Compose با زبان کاتلین

تحلیل و بررسی
آموزش ساخت انیمیشن در Jetpack Compose با زبان کاتلین

مقدمه: اهمیت انیمیشن های هدفمند در تجربه کاربری و طراحی مدرن اندروید

در مهندسی فرانت اند و توسعه نرم افزار های مدرن موبایل، انیمیشن صرفا ابزاری برای زیبایی ظاهری نیست. حرکات هوشمندانه بصری باعث هدایت توجه کاربر، درک بهتر تغییرات مدیریت State در Jetpack Compose صفحه و ایجاد حس پویایی در تعامل با المان ها می گردند.

با معرفی فریم ورک نوین جت پک کامپوز، رویکرد دستوری سنتی مبتنی بر XML کاملا کنار رفت. اکنون در بستر کاتلین و معماری اعلانی، انیمیشن ها به طور مستقیم با متغیرهای وضعیت یا همان State پیوند خورده اند.

در این سیستم، شما فقط وضعیت پایانی المان را معین می کنید. موتور رندرینگ کامپوز وظیفه درونیابی فریم به فریم و زمان بندی دقیق حرکات را با رفرش ریت نمایشگر گوشی بر عهده می گیرد.

انیمیشن هایی که بدون درک چرخه حیات کامپوز پیاده سازی شوند، منجر به کاهش Recomposition در اندروید های مکرر و تخلیه سریع باتری خواهند شد. هدف این مقاله، آموزش تکنیک های بهینه از پایه تا سطح پیشرفته است.

تعریف سریع و معماری انیمیشن در Jetpack Compose چیست؟

پاسخ سریع و معماری: سیستم انیمیشن در Jetpack Compose مجموعه‌ای از توابع اعلانی مبتنی بر همزمانی کاتلین است که وضعیت ظاهری مولفه ها را بر اساس تغییر مقادیر State به صورت نرم بازتولید می‌کند.

این ابزار ها در دو لایه سطح بالا (مانند AnimatedVisibility و AnimatedContent) و سطح پایین (مانند animate*AsState و updateTransition) سازمان‌دهی شده‌اند تا کنترل کاملی بر فیزیک فنر و نرخ ۱۲۰ هرتز فراهم سازند.

نکته کلیدی در معماری اعلانی

انیمیشن در کامپوز یک فرآیند ایزوله نیست، بلکه نتیجه مستقیم تغییرات در فازهای سه‌گانه کامپوزیشن، لایوت و دراو است. با متصل کردن انیمیشن به فازهای مناسب، پردازش های گرافیکی مستقیما به پردازنده گرافیکی یا همان GPU منتقل می‌شوند.

پیشنهاد مطالعه
آموزش جامع Jetpack Compose برای توسعه رابط کاربری اندروید
آموزش جت پک کامپوز Jetpack Compose در اندروید؛ معماری رابط کاربری اعلانی، مدیریت State و جریان UDF، فازهای Recomposition، ناوبری تایپ سیف و نمونه کده…
مشاهده مقاله ←

ساختار سلسله مراتب و دسته بندی ابزار های انیمیشن در کامپوز

برای انتخاب ابزار بهینه، ابتدا باید سطوح دسترسی و لایه های انتزاعی مختلف فریم ورک را بشناسیم. شکل زیر نمایی کلی از این سازمان‌دهی را نشان می‌دهد:

ساختار سلسله مراتب ابزار های انیمیشن در Jetpack Compose
شکل ۱: دسته بندی ابزار های سطح بالا و سطح پایین انیمیشن در معماری Jetpack Compose

ابزار های سطح بالا به شما اجازه می‌دهند بدون درگیر شدن با جزئیات عددی، ورود و خروج کامپوننت ها را با یک خط کد پیاده سازی کنید. در مقابل، توابع سطح پایین کنترل مقادیر ریاضی و محاسبات فیزیکی را در اختیارتان می‌گذارند.

این تفکیک لایه ها به توسعه دهنده امکان می‌دهد با کمترین پیچیدگی کدهای پایدار و بهینه‌ای تولید کند که خوانایی بالایی در بررسی کد دارند.

کار با AnimatedVisibility: انیمیشن های ورود و خروج مولفه ها

رایج‌ترین نیاز در توسعه برنامه هایی اندروید، ظاهر یا پنهان شدن یک المان بر اساس شرط بولین است. در رویکرد سنتی از ویزیبیلیتی استفاده می‌شد که پرش های ناگهانی ایجاد می‌کرد.

کامپوزبل AnimatedVisibility به صورت پیش‌فرض ترنزیشن های محو شدن یا همان fadeIn و بزرگ شدن را ترکیب می‌کند. شما می‌توانید این رفتار را به طور کامل سفارشی سازی نمایید:

Kotlin ui/NotificationBanner.kt
@Composable
fun NotificationBanner(
    isVisible: Boolean,
    message: String,
    modifier: Modifier = Modifier
) {
    AnimatedVisibility(
        visible = isVisible,
        enter = fadeIn(animationSpec = tween(300)) +
                slideInVertically(initialOffsetY = { -it }),
        exit = fadeOut(animationSpec = tween(200)) +
               slideOutVertically(targetOffsetY = { -it }),
        modifier = modifier
    ) {
        Surface(
            color = MaterialTheme.colorScheme.primaryContainer,
            shape = RoundedCornerShape(12.dp)
        ) {
            Text(
                text = message,
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

با عملگر + می‌توان افکت های حرکتی گوناگون را با هم ترکیب کرد تا المان به شکل همزمان اسلاید و محو گردد.

ترکیب ترنزیشن ها و کالبدشکافی منحنی های حرکتی در فاز ورود و خروج

در هنگام اجرای انیمیشن های ورود و خروج، زیرسیستم گرافیکی کامپوز چندین فاز حرکتی مستقل را مدیریت می‌نماید. شکل زیر جریان پردازش این ترنزیشن ها را نشان می‌دهد:

چرخه ترکیب ترنزیشن ها در AnimatedVisibility کامپوز
شکل ۲: پایپ لاین ترکیب ترنزیشن های محو شدن، اسلاید و تغییر ابعاد در AnimatedVisibility

ترکیب افکت های slideInHorizontally، expandVertically و scaleIn باعث روان‌تر شدن نمایش کارت های لیست و دیالوگ ها می‌شود.

هر کدام از این توابع دارای پارامتر animationSpec اختصاصی هستند که کنترل دقیق سرعت و تاخیر زمانی را مهیا می‌سازد.

انیمیشن اختصاصی فرزندان با AnimatedVisibilityScope و animateEnterExit

گاهی اوقات قصد دارید المان های درون یک کانتینر ترنزیشن های متفاوتی نسبت به والد خود داشته باشند. برای مثال عنوان کانتینر از چپ وارد شود در حالی که آیکون آن از راست اسلاید کند.

بدنه تابع AnimatedVisibility یک اسکوپ اختصاصی به نام AnimatedVisibilityScope فراهم می‌کند. به کمک این اسکوپ، مودیفایر Modifier.animateEnterExit() روی هر المان فرزند قابل استفاده است:

Kotlin ui/StaggeredHeader.kt
@Composable
fun StaggeredHeader(visible: Boolean) {
    AnimatedVisibility(
        visible = visible,
        enter = fadeIn(),
        exit = fadeOut()
    ) {
        Row(modifier = Modifier.padding(16.dp)) {
            Text(
                text = "خوش آمدید",
                modifier = Modifier.animateEnterExit(
                    enter = slideInHorizontally { -it }
                )
            )
            Spacer(modifier = Modifier.width(8.dp))
            Icon(
                Icons.Default.Check,
                contentDescription = null,
                modifier = Modifier.animateEnterExit(
                    enter = scaleIn()
                )
            )
        }
    }
}

این ویژگی امکان ایجاد انیمیشن های چندمرحله‌ای (Staggered Animations) بسیار جذاب را بدون نیاز به کدهای پیچیده میسر می‌سازد.

مدیریت تغییر اندازه خودکار کانتینر با مودیفایر animateContentSize

بسیاری از کامپوننت ها مانند باکس های بازشونده یا متن های طولانی دارای ابعاد متغیر هستند. با اضافه کردن مودیفایر animateContentSize، هر تغییری در سایز فرزندان به صورت نرم انیمیت خواهد شد:

Kotlin ui/ExpandableCard.kt
@Composable
fun ExpandableCard(title: String, desc: String) {
    var expanded by remember { mutableStateOf(false) }

    Card(
        modifier = Modifier
            .fillMaxWidth()
            .animateContentSize(
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow
                )
            )
            .clickable { expanded = !expanded }
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = title, style = MaterialTheme.typography.titleMedium)
            if (expanded) {
                Spacer(modifier = Modifier.height(8.dp))
                Text(text = desc)
            }
        }
    }
}

این مودیفایر در فاز لایوت فعال می‌شود و از پرش های زننده در ساختار صفحه جلوگیری به عمل می‌آورد.

سوییچ روان بین محتواهای وابسته به وضعیت با AnimatedContent

اگر بر اساس یک مقدار مشخص (مانند تب های فعال، تبادل صفحات یا شمارش اعداد) قصد نمایش کامپوننت های مختلف را دارید، AnimatedContent بهترین گزینه است:

Kotlin ui/AnimatedCounter.kt
@Composable
fun AnimatedCounter(count: Int) {
    AnimatedContent(
        targetState = count,
        transitionSpec = {
            if (targetState > initialState) {
                (slideInVertically { it } + fadeIn()).togetherWith(
                    slideOutVertically { -it } + fadeOut()
                )
            } else {
                (slideInVertically { -it } + fadeIn()).togetherWith(
                    slideOutVertically { it } + fadeOut()
                )
            }
        },
        label = "CounterAnimation"
    ) { targetCount ->
        Text(
            text = "$targetCount",
            style = MaterialTheme.typography.headlineLarge
        )
    }
}

تابع togetherWith در کامپوز نحوه تعویض دو محتوا را مشخص می‌سازد و افکتی همانند شمارنده کیلومترشمار اتومبیل پدید می‌آورد.

انیمیشن های تک مقداری با مجموعه توابع قدرتمند animate*AsState

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

کامپوز نسخه های متعددی از این تابع را در اختیار شما قرار می‌دهد:

  • animateFloatAsState: برای چرخش، مقیاس و درجه شفافیت المان.
  • animateColorAsState: برای تغییر نرم پالت های رنگی تم تاریک و روشن.
  • animateDpAsState: برای پدینگ، اندازه گوشه ها و ارتفاع سایه.
  • animateOffsetAsState: برای جابجایی المان ها در محورهای مختصات دو بعدی.
Kotlin ui/ScaleButton.kt
@Composable
fun PulsingActionButton(isSelected: Boolean, onClick: () -> Unit) {
    val scale by animateFloatAsState(
        targetValue = if (isSelected) 1.15f else 1.0f,
        animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
        label = "ScaleAnim"
    )
    val color by animateColorAsState(
        targetValue = if (isSelected) Color(0xFF2563EB) else Color(0xFF64748B),
        label = "ColorAnim"
    )

    Box(
        modifier = Modifier
            .scale(scale)
            .background(color, CircleShape)
            .clickable(onClick = onClick)
            .padding(16.dp)
    ) {
        Icon(Icons.Default.Star, contentDescription = null, tint = Color.White)
    }
}

مقدار خروجی این توابع یک شیء State است که با کلمه کلیدی by به عنوان متغیر معمولی خوانده می‌شود.

پیکربندی انواع AnimationSpec: از Tween تا Keyframes و Easing

هر انیمیشن نیازمند یک الگوی ریاضی برای توزیع سرعت در واحد زمان است. با پارامتر animationSpec نوع شتاب‌دهی را مشخص می‌کنیم:

  • tween: انیمیشن زمانی خطی با مدت زمان معین بر حسب میلی‌ثانیه و منحنی های ایزینگ استاندارد.
  • spring: فیزیک طبیعی فنر بدون تعیین مدت زمان قطعی که حرکتی واقع‌گرایانه ایجاد می‌کند.
  • keyframes: تعریف نقاط عطف دقیق در طول زمان برای حرکات چندمرحله‌ای و پیچیده.
  • snap: تغییر آنی و بدون انیمیشن برای موقعیت های فوری یا تنظیمات دسترسی پذیری.
Kotlin ui/CustomKeyframe.kt
val keyframeSpec = keyframes {
    durationMillis = 600
    0.0f at 0 with LinearOutSlowInEasing
    0.8f at 200 with FastOutLinearInEasing
    0.5f at 350
    1.0f at 600
}

منحنی های شتابدهی سفارشی با CubicBezierEasing و متریال دیزاین ۳

در سیستم متریال ۳، انیمیشن ها از منحنی های بزیه درجه سه یا همان CubicBezierEasing برای شبیه سازی حرکت طبیعی اجسام بهره می‌برند. چهار ضریب کنترلی این منحنی ها نحوه شتاب‌گیری و توقف را معین می‌نمایند.

سه الگوی پیش‌فرض استاندارد در متریال شامل موارد زیر هستند:

  • EmphasizedDecelerate: شتاب اولیه سریع و توقف آرام برای المان های ورودی به صفحه.
  • EmphasizedAccelerate: شروع آرام و خروج با شتاب برای المان‌هایی که از کادر محو می‌شوند.
  • StandardEasing: حرکت ملایم در شروع و پایان، مناسب برای جابجایی دکمه ها و تغییر اندازه تب ها.
Kotlin ui/Material3Curves.kt
val customM3Curve = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1.0f)

val m3TweenSpec = tween(
    durationMillis = 400,
    easing = customM3Curve
)

ماشین حالت چندگانه با updateTransition برای هماهنگی ویژگی ها

هنگامی که یک المان وضعیت های گوناگونی دارد و در هر وضعیت چندین خصوصیت (رنگ، گوشه ها، سایز و چرخش) باید به صورت همزمان انیمیت شوند، استفاده مجزا از چند تابع مستقل باعث ناهماهنگی حرکتی می‌گردد.

در این شرایط، updateTransition به عنوان یک ماشین وضعیت جامع تمامی مقادیر را به صورت یکپارچه همگام می‌سازد:

Kotlin ui/StateTransitionCard.kt
enum class CardState { Normal, Highlighted }

@Composable
fun StateTransitionCard(state: CardState) {
    val transition = updateTransition(targetState = state, label = "CardState")

    val radius by transition.animateDp(label = "Radius") { s ->
        if (s == CardState.Normal) 8.dp else 24.dp
    }
    val bg by transition.animateColor(label = "Color") { s ->
        if (s == CardState.Normal) Color(0xFF1E293B) else Color(0xFF3B82F6)
    }

    Surface(
        shape = RoundedCornerShape(radius),
        color = bg,
        modifier = Modifier.size(160.dp)
    ) {
        // محتوای کارت
    }
}

کالبدشکافی عملکرد ماشین حالت با updateTransition در کامپوز

تمامی خصوصیات تعریف‌شده در updateTransition از تایم لاین مشترک پیروی می‌کنند. شکل زیر نحوه هماهنگی متغیرها را نمایش می‌دهد:

ماشین حالت چندگانه با updateTransition در Jetpack Compose
شکل ۳: همگام سازی چند ویژگی UI در یک ماشین حالت هماهنگ با updateTransition

علاوه بر این، در محیط اندروید استودیو ابزار Animation Preview مستقیما به شما اجازه می‌دهد فریم های انیمیشن را به صورت زنده دیباگ کنید.

شما می‌توانید انیمیشن را فریم به فریم جلو برده و بررسی کنید که آیا در فواصل میانی تداخلی در چیدمان رخ می‌دهد یا خیر.

انیمیشن های تکرارشونده و افکت های مداوم با rememberInfiniteTransition

برای ایجاد انیمیشن های پیوسته مانند افکت لودینگ شیمر یا چرخش نشانگر بارگذاری، از تابع rememberInfiniteTransition استفاده می‌کنیم:

Kotlin ui/ShimmerPlaceholder.kt
@Composable
fun ShimmerPlaceholder(modifier: Modifier = Modifier) {
    val infiniteTransition = rememberInfiniteTransition(label = "Shimmer")
    val alpha by infiniteTransition.animateFloat(
        initialValue = 0.3f,
        targetValue = 0.9f,
        animationSpec = infiniteRepeatable(
            animation = tween(800, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "Alpha"
    )

    Box(
        modifier = modifier
            .background(Color.LightGray.copy(alpha = alpha), RoundedCornerShape(8.dp))
    )
}

تنظیم RepeatMode.Reverse باعث می‌شود مقدار پس از رسیدن به نقطه اوج، به نرمی به مقدار آغازین بازگردد.

انیمیشن حاشیه درخشان (Glowing Border) و شیب رنگ گردان با Canvas

یکی از زیباترین افکت های ترند در برنامه هایی مدرن، حاشیه های درخشان نئونی با شیب رنگ چرخان است. با ترکیب rememberInfiniteTransition و متد drawWithContent در لایه دراو این افکت رندر می‌شود:

Kotlin ui/GlowingBorder.kt
@Composable
fun GlowingBorderCard(content: @Composable () -> Unit) {
    val transition = rememberInfiniteTransition(label = "Neon")
    val angle by transition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            animation = tween(2500, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        ),
        label = "Angle"
    )

    Box(
        modifier = Modifier
            .padding(2.dp)
            .drawWithContent {
                rotate(angle) {
                    drawCircle(
                        brush = Brush.sweepGradient(
                            listOf(Color.Cyan, Color.Magenta, Color.Cyan)
                        )
                    )
                }
                drawContent()
            }
    ) {
        content()
    }
}

به دلیل اجرای این کد در فاز دراو، هیچ سربار اضافی به پردازنده تحمیل نمی‌شود.

سیستم فیزیک فنر (Spring Physics): پارامترهای DampingRatio و Stiffness

در سیستم های طراحی مدرن، انیمیشن های زمانی خطی جای خود را به فیزیک فنر داده‌اند تا حرکات المان ها حسی طبیعی و زنده به کاربر منتقل کنند.

فیزیک فنر در کامپوز با دو ضریب اساسی کنترل می‌شود:

  • DampingRatio (نسبت میرایی): میزان جهش و پرش فنر پس از رها شدن را معین می‌کند:
    • Spring.DampingRatioHighBouncy: پرش زیاد مناسب دکمه های تایید و ایموجی ها.
    • Spring.DampingRatioMediumBouncy: پرش ملایم و ارگونومیک برای کارت ها و کشوها.
    • Spring.DampingRatioNoBouncy: توقف بدون پرش مناسب صفحات رسمی و پنل ها.
  • Stiffness (سفتی یا سختی فنر): سرعت بازگشت به نقطه تعادل را مشخص می‌سازد:
    • Spring.StiffnessHigh: بازگشت بسیار سریع در کسر ثانیه.
    • Spring.StiffnessMedium: سرعت متعادل برای اکثر بخش های برنامه.
    • Spring.StiffnessLow: حرکت آرام و سنگین برای المان های بزرگ.

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

اتصال ژست های لمسی به انیمیشن با Animatable و Drag Gestures

در سناریوهایی نظیر کشیدن کارت به چپ و راست برای حذف، المان باید دقیقا زیر انگشت کاربر حرکت کند. شکل زیر معماری تعاملی اتصال ژست به کلاس Animatable را نشان می‌دهد:

معماری اتصال ژست های لمسی به Animatable در کامپوز
شکل ۴: مدیریت رویدادهای Drag و انیمیت فنری بازگشت با Animatable و Coroutine Scope

کلاس Animatable به عنوان هسته سیستم انیمیشن، امکان کنترل دستی و اجرای تعلیقی توابع در قالب کروتین ها را مهیا می‌سازد.

این کلاس به شما اجازه می‌دهد انیمیشن های در حال اجرا را در صورت لمس مجدد کاربر متوقف ساخته و کنترل فیزیکی را بدون لگ به دست بگیرید.

پیاده سازی کد تعاملی Swipe to Dismiss با کلاس هسته Animatable

در کد زیر مشاهده می‌کنید که چطور با مودیفایر pointerInput مقادیر جابجایی انگشت را رصد کرده و در پایان با انیمیشن فنری بازمی‌گردانیم:

Kotlin ui/InteractiveSwipeCard.kt
@Composable
fun InteractiveSwipeCard(onDismiss: () -> Unit) {
    val offsetX = remember { Animatable(0f) }
    val scope = rememberCoroutineScope()

    Box(
        modifier = Modifier
            .offset { IntOffset(offsetX.value.roundToInt(), 0) }
            .pointerInput(Unit) {
                detectDragGestures(
                    onDrag = { change, dragAmount ->
                        change.consume()
                        scope.launch { offsetX.snapTo(offsetX.value + dragAmount.x) }
                    },
                    onDragEnd = {
                        scope.launch {
                            if (abs(offsetX.value) > 250f) {
                                val target = if (offsetX.value > 0) 1000f else -1000f
                                offsetX.animateTo(target, tween(200))
                                onDismiss()
                            } else {
                                offsetX.animateTo(0f, spring(Spring.DampingRatioMediumBouncy))
                            }
                        }
                    }
                )
            }
    ) {
        // محتوای بصری کارت
    }
}

متد snapTo مقدار را فورا و بدون انیمیشن تنظیم می‌کند تا المان چسبیده به انگشت حرکت نماید.

انیمیشن های اینرسی و پرتاب (Decay Animation) با FlingBehavior

زمانی که کاربر دست خود را با سرعت روی صفحه می‌کشد و رها می‌کند (Fling Gesture)، المان نباید ناگهان متوقف شود؛ بلکه باید با یک اینرسی طبیعی سرعت خود را از دست بدهد.

برای این منظور کامپوز کلاس DecayAnimationSpec و تابع rememberSplineBasedDecay را فراهم کرده است که همان الگوریتم فرود طبیعی اسکرول لیست های اندروید را پیاده سازی می‌کند:

Kotlin ui/DecayFling.kt
@Composable
fun FlingBox() {
    val offset = remember { Animatable(0f) }
    val decay = rememberSplineBasedDecay()
    val scope = rememberCoroutineScope()

    Box(
        modifier = Modifier
            .offset { IntOffset(offset.value.roundToInt(), 0) }
            .pointerInput(Unit) {
                detectHorizontalDragGestures { _, dragAmount ->
                    scope.launch {
                        offset.snapTo(offset.value + dragAmount)
                    }
                }
            }
    )
}

استفاده از دیکی انیمیشن حس فیزیکی دنیای واقعی را شبیه سازی کرده و رضایت کاربری را به طور چشمگیری افزایش می‌دهد.

ترنزیشن های عنصر مشترک (Shared Element Transitions) در کامپوز

یکی از جذاب‌ترین تکنیک های انیمیشن در سیستم متریال، انتقال پیوسته یک المان (مانند تصویر محصول) از صفحه لیست به صفحه جزئیات است.

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

Kotlin ui/SharedProductItem.kt
@Composable
fun SharedTransitionScope.ProductItem(
    item: Product,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    Image(
        painter = painterResource(item.imageRes),
        contentDescription = item.title,
        modifier = Modifier
            .sharedElement(
                state = rememberSharedContentState(key = "item-${item.id}"),
                animatedVisibilityScope = animatedVisibilityScope
            )
            .size(100.dp)
    )
}

این رویکرد تداوم بصری بی‌نظیری خلق می‌کند و حس گسستگی میان صفحات ناوبری را کاملا از میان برمی‌دارد.

میکرو اینتراکشن ها (Micro-interactions): واکنش لمسی دکمه با InteractionSource

در برنامه هایی باکیفیت، دکمه ها و المان های کلیک پذیر با فشرده شدن انگشت کاربر اندکی منقبض شده و با رها شدن جهش کوچکی انجام می‌دهند.

با گوش دادن به جریان MutableInteractionSource و متد collectIsPressedAsState، می‌توان مقیاس دکمه را به شکل زیر انیمیت کرد:

Kotlin ui/BouncyButton.kt
@Composable
fun BouncyButton(onClick: () -> Unit, content: @Composable () -> Unit) {
    val interactionSource = remember { MutableInteractionSource() }
    val isPressed by interactionSource.collectIsPressedAsState()

    val scale by animateFloatAsState(
        targetValue = if (isPressed) 0.92f else 1.0f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        ),
        label = "BounceScale"
    )

    Box(
        modifier = Modifier
            .scale(scale)
            .clickable(
                interactionSource = interactionSource,
                indication = null,
                onClick = onClick
            )
    ) {
        content()
    }
}

این واکنش لمسی کوچک تاثیر روانی شگرفی در ایجاد حس پاسخگویی و زنده بودن برنامه در ذهن کاربر برجای می‌گذارد.

همچنین با اتصال بازخورد لرزشی یا همان HapticFeedback در لحظه کلیک، کاربر حس فشردن یک دکمه فیزیکی واقعی را لمس خواهد کرد.

انیمیشن وکتورهای متحرک با AnimatedVectorDrawable در کامپوز

برای انیمیت کردن آیکون های تعاملی مانند تبدیل آیکون منوی همبرگری به علامت ضربدر یا تبدیل آیکون پخش به مکث، استفاده از وکتورهای متحرک کارآمدترین روش است.

در کامپوز، با استفاده از متد rememberAnimatedVectorPainter می‌توان فایل های وکتور متحرک را بارگذاری و بر اساس وضعیت تغییر داد:

Kotlin ui/PlayPauseIcon.kt
@Composable
fun AnimatedPlayPauseButton(isPlaying: Boolean, onClick: () -> Unit) {
    val image = AnimatedImageVector.load(
        R.drawable.avd_play_to_pause
    )
    val painter = rememberAnimatedVectorPainter(
        animatedImageVector = image,
        atEnd = isPlaying
    )

    IconButton(onClick = onClick) {
        Icon(
            painter = painter,
            contentDescription = "پخش یا توقف",
            tint = MaterialTheme.colorScheme.primary
        )
    }
}

این وکتورها به صورت برداری رندر می‌شوند و بر روی تمامی چگالی های پیکسلی بدون کوچک‌ترین تاری یا افت کیفیت اجرا می‌گردند.

امواج راداری متحرک (Pulsing Radar Rings) با تداخل فازهای زمانی Canvas

در صفحات تماس ورودی یا اسکن بلوتوث، نمایش حلقه های متحدالمرکزی که از مرکز به بیرون منبسط می‌شوند نیازمند محاسبه شعاع و شفافیت همزمان است.

با تعریف چندین فاز زمانی با اختلاف فاز مشخص در rememberInfiniteTransition، این افکت جذاب بدون افت فریم پیاده سازی می‌شود:

Kotlin ui/RadarWave.kt
@Composable
fun PulsingRadarRing(modifier: Modifier = Modifier) {
    val transition = rememberInfiniteTransition(label = "Radar")
    val radius by transition.animateFloat(
        initialValue = 20f,
        targetValue = 180f,
        animationSpec = infiniteRepeatable(
            animation = tween(2000, easing = LinearOutSlowInEasing),
            repeatMode = RepeatMode.Restart
        ),
        label = "Radius"
    )
    val alpha by transition.animateFloat(
        initialValue = 0.8f,
        targetValue = 0f,
        animationSpec = infiniteRepeatable(
            animation = tween(2000, easing = LinearOutSlowInEasing),
            repeatMode = RepeatMode.Restart
        ),
        label = "Alpha"
    )

    Canvas(modifier = modifier.size(200.dp)) {
        drawCircle(
            color = Color(0xFF3B82F6).copy(alpha = alpha),
            radius = radius
        )
    }
}

با افزایش شعاع دایره، شفافیت آن کاهش می‌یابد تا المان در فضای خالی اطراف محو گردد.

بهینه سازی کارایی: جلوگیری از Recomposition های غیرضروری در انیمیشن

بزرگترین اشتباه در انیمیشن های کامپوز، خواندن مقدار انیمیت شونده در فاز کامپوزیشن است. این امر باعث می‌شود تابع @Composable در هر فریم دوباره اجرا شود که موجب افت شدید فریم و خالی شدن باتری می‌گردد.

برای دستیابی به عملکرد بدون لگ و تثبیت نرخ ۱۲۰ فریم بر ثانیه، همیشه باید خواندن مقدار را به فاز لایوت یا دراو موکول کنید:

Kotlin ui/OptimizedModifiers.kt
// اشتباه: بازتولید مکرر در هر فریم
Modifier.offset(x = animatedOffset.dp)

// کاملا استاندارد: اجرای مستقیم در فاز Layout بدون ری کامپوزیشن
Modifier.offset { IntOffset(x = animatedOffset.roundToInt(), y = 0) }

// اجرای فوق سریع مستقیم بر روی پردازنده گرافیکی GPU
Modifier.graphicsLayer {
    alpha = animatedAlpha
    rotationZ = animatedRotation
}
هشدار مهم در مانیتورینگ فریم ها

هرگز در بدنه توابعی که انیمیشن دارند اشیاء سنگین مانند فرمت‌کننده های تاریخ یا لیست های جدید ایجاد نکنید. این کار باعث بارگذاری بیش از حد روی زباله‌روب اندروید یا همان Garbage Collector شده و لگ های موسوم به Jank ایجاد می‌کند.

بنچمارک و پایش نرخ فریم با Macrobenchmark و Android Studio Profiler

برای سنجش استاندارد نرخ فریم در دستگاه های واقعی، ابزار Macrobenchmark به همراه سناریوهای آزمون حرکتی توصیه می‌شود. با استفاده از سناریوی CompilationMode.Full و اندازه‌گیری متریک FrameTimingMetric، درصد فریم های از دست رفته سنجیده می‌شود.

در پروژه هایی بزرگ مبتنی بر معماری تمیز، منطق داده‌ای انیمیشن از رابط کاربری تفکیک شده و تنها وضعیت های نهایی به لایه یوآی تزریق می‌گردند.

این استراتژی باعث می‌شود منطق انیمیشن در آزمون های واحد قابل ارزیابی باشد و خطاهای حرکتی پیش از استقرار در محیط پروداکشن کشف گردند.

جدول مقایسه جامع: تفاوت و کاربرد ابزار های انیمیشن در Jetpack Compose

در جدول زیر، تمامی ابزار های انیمیشن کامپوز بر اساس میزان کنترل، تعداد مقادیر و موارد کاربرد مقایسه شده‌اند:

نام ابزار / API سطح انتزاع تعداد مقادیر نوع ترنزیشن بهترین کاربرد عملیاتی
AnimatedVisibility سطح بالا کانتینر کامل Layout Enter / Exit نمایش و پنهان سازی بنرها و کارت ها
animateContentSize سطح بالا (مودیفایر) عرض و ارتفاع Layout Shift باکس های بازشونده و کارت های آکاردئونی
AnimatedContent سطح بالا محتواهای متوالی Content Transform شمارنده ها و فرم های مرحله‌ای
animate*AsState سطح میانی ۱ مقدار تکی Target Driven تغییر رنگ، مقیاس دکمه و شفافیت
updateTransition سطح پایین چندین مقدار همزمان State Machine ماشین وضعیت های پیچیده کارت ها
rememberInfiniteTransition سطح میانی ۱ یا چند مقدار Infinite Loop لودینگ های شیمر و انیمیشن نبض
Animatable سطح پایین (Core) ۱ مقدار ریاضی Physics / Coroutine ژست های لمسی درگ و فیزیک پرتاب
پیشنهاد مطالعه
آموزش State و Side Effects در Jetpack Compose با کاتلین
آموزش جامع مدیریت State و هندلینگ Side Effects در Jetpack Compose با کاتلین. بررسی عمیق LaunchedEffect، DisposableEffect، derivedStateOf، snapshotFlo…
مشاهده مقاله ←

اشتباهات رایج توسعه دهندگان اندروید در انیمیشن های کامپوز و راه حل ها

بررسی پروژه هایی واقعی نشان می‌دهد که رعایت ۴ اصل زیر مانع از بروز مشکلات رایج عملکردی می‌گردد:

  1. اجرای انیمیشن های بی‌نهایت در تب های پس‌زمینه: استفاده از rememberInfiniteTransition در کامپوننت های مخفی باعث مصرف مداوم باتری می‌شود.
  2. استفاده از ترنزیشن های زمانی طولانی: به جای استفاده از مدت زمان های طولانی در tween، همواره از فیزیک فنر برای ایجاد حرکات ارگونومیک بهره ببرید.
  3. عدم درج برچسب در توابع انیمیشن: ننوشتن پارامتر label امکان دیباگ در تب انیمیشن اندروید استودیو را سلب می‌کند.
  4. بازتولید اشیاء در بدنه: از تعریف نمونه های جدید در هر فریم اجتناب نموده و وضعیت ها را با remember تثبیت کنید.

برای آشنایی با انتخاب ابزار های بهینه توسعه، مقاله بهترین زبان برنامه نویسی اندروید راهنمای مفیدی خواهد بود.

همچنین برای فراگیری جامع‌تر مسیر یادگیری، مطالعه نقشه راه برنامه نویسی اندروید دید کاملی از اکوسیستم توسعه نرم افزار موبایل ارائه می‌دهد.

رعایت استانداردهای طراحی متریال ۳، تست فریم ها روی سخت افزارهای میان‌رده و استفاده از پروفایلر رسمی گوگل، تفاوت میان یک اپلیکیشن معمولی و یک شاهکار مهندسی نرم افزار را رقم می‌زند.

با به‌کارگیری ابزار های اعلانی کامپوز، خلق رابط های کاربری چشم‌نواز با عملکرد روان و بدون افت فریم دیگر یک رویا نیست بلکه یک مزیت رقابتی پایدار است.

پرسش های متداول درباره انیمیشن در Jetpack Compose (FAQ)

۱. تفاوت اصلی animate*AsState با updateTransition چیست؟

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

۲. چرا استفاده از Modifier.graphicsLayer برای انیمیشن سریع تر است؟

زیرا تغییراتی نظیر آلفا و چرخش در graphicsLayer مستقیما توسط GPU اجرا شده و فازهای لایوت و کامپوزیشن در CPU دور زده می‌شوند.

۳. چگونه تغییر اندازه خودکار کارت را در کامپوز نرم کنیم؟

با افزودن مودیفایر Modifier.animateContentSize() به کانتینر والد، هرگونه تغییر در ابعاد ناشی از فرزندان به صورت خودکار انیمیت می‌گردد.

۴. بهترین ابزار برای ساخت لودینگ شیمر در کامپوز چیست؟

استفاده از تابع rememberInfiniteTransition به همراه animateFloat و تکرار معکوس مناسب‌ترین راهکار کم‌مصرف است.

۵. تفاوت DampingRatio و Stiffness در فیزیک فنر چیست؟

پارامتر DampingRatio میزان جهش و پرش فنر را کنترل می‌کند و Stiffness سرعت واکنش و بازگشت فنر به حالت سکون را تعیین می‌نماید.

۶. برای اتصال ژست های لمسی به انیمیشن از چه ابزاری استفاده می شود؟

کلاس Animatable به همراه مودیفایر pointerInput و متد detectDragGestures استانداردترین ترکیب برای حرکات لمسی است.

۷. چطور در کامپوز انیمیشن جابجایی تب ها را پیاده سازی کنیم؟

با استفاده از کامپوزبل AnimatedContent و تعریف رفتار ورود و خروج محتوا به کمک متد togetherWith ترنزیشن های دلخواه شکل می‌گیرند.

۸. آیا انیمیشن های کامپوز از نمایشگرهای ۱۲۰ هرتزی پشتیبانی می کنند؟

بله، موتور رندرینگ کامپوز با رفرش ریت صفحه نمایشگر دستگاه همگام بوده و در صورت کد نویسی بهینه با نرخ ۱۲۰ فریم بر ثانیه رندر می‌شود.

۹. در AnimatedVisibility چطور چندین ترنزیشن را همزمان اعمال کنیم؟

با عملگر پلاس می‌توان ترنزیشن های گوناگون نظیر محو شدن و اسلاید عمودی را با یکدیگر ترکیب نمود.

۱۰. آیا می توان انیمیشن های کامپوز را در محیط اندروید استودیو تست کرد؟

بله، با کلیک روی آیکون انیمیشن در پنجره پیش‌نمایش کامپوز، تب Animation Preview فعال شده و امکان بررسی تایم لاین فریم به فریم فراهم می‌گردد.

۱۱. چطور انیمیشن را بر اساس تنظیمات سیستم عامل کاربر غیرفعال کنیم؟

با بررسی مقدار مقیاس زمانی انیمیشن در سیستم عامل و قرار دادن زمان ترنزیشن ها بر روی صفر میلی‌ثانیه در زمان فعال بودن گزینه کاهش انیمیشن.

امتیاز و نظرات

۰.۰ (۰ نظر)
برای ثبت دیدگاه باید وارد حساب کاربری شوید. ورود

هنوز نظری ثبت نشده است. اولین نفر باشید.