مقدمه: اهمیت انیمیشن های هدفمند در تجربه کاربری و طراحی مدرن اندروید
در مهندسی فرانت اند و توسعه نرم افزار های مدرن موبایل، انیمیشن صرفا ابزاری برای زیبایی ظاهری نیست. حرکات هوشمندانه بصری باعث هدایت توجه کاربر، درک بهتر تغییرات مدیریت State در Jetpack Compose صفحه و ایجاد حس پویایی در تعامل با المان ها می گردند.
با معرفی فریم ورک نوین جت پک کامپوز، رویکرد دستوری سنتی مبتنی بر XML کاملا کنار رفت. اکنون در بستر کاتلین و معماری اعلانی، انیمیشن ها به طور مستقیم با متغیرهای وضعیت یا همان State پیوند خورده اند.
در این سیستم، شما فقط وضعیت پایانی المان را معین می کنید. موتور رندرینگ کامپوز وظیفه درونیابی فریم به فریم و زمان بندی دقیق حرکات را با رفرش ریت نمایشگر گوشی بر عهده می گیرد.
انیمیشن هایی که بدون درک چرخه حیات کامپوز پیاده سازی شوند، منجر به کاهش Recomposition در اندروید های مکرر و تخلیه سریع باتری خواهند شد. هدف این مقاله، آموزش تکنیک های بهینه از پایه تا سطح پیشرفته است.
تعریف سریع و معماری انیمیشن در Jetpack Compose چیست؟
پاسخ سریع و معماری: سیستم انیمیشن در Jetpack Compose مجموعهای از توابع اعلانی مبتنی بر همزمانی کاتلین است که وضعیت ظاهری مولفه ها را بر اساس تغییر مقادیر State به صورت نرم بازتولید میکند.
این ابزار ها در دو لایه سطح بالا (مانند AnimatedVisibility و AnimatedContent) و سطح پایین (مانند animate*AsState و updateTransition) سازماندهی شدهاند تا کنترل کاملی بر فیزیک فنر و نرخ ۱۲۰ هرتز فراهم سازند.
انیمیشن در کامپوز یک فرآیند ایزوله نیست، بلکه نتیجه مستقیم تغییرات در فازهای سهگانه کامپوزیشن، لایوت و دراو است. با متصل کردن انیمیشن به فازهای مناسب، پردازش های گرافیکی مستقیما به پردازنده گرافیکی یا همان GPU منتقل میشوند.
ساختار سلسله مراتب و دسته بندی ابزار های انیمیشن در کامپوز
برای انتخاب ابزار بهینه، ابتدا باید سطوح دسترسی و لایه های انتزاعی مختلف فریم ورک را بشناسیم. شکل زیر نمایی کلی از این سازماندهی را نشان میدهد:
ابزار های سطح بالا به شما اجازه میدهند بدون درگیر شدن با جزئیات عددی، ورود و خروج کامپوننت ها را با یک خط کد پیاده سازی کنید. در مقابل، توابع سطح پایین کنترل مقادیر ریاضی و محاسبات فیزیکی را در اختیارتان میگذارند.
این تفکیک لایه ها به توسعه دهنده امکان میدهد با کمترین پیچیدگی کدهای پایدار و بهینهای تولید کند که خوانایی بالایی در بررسی کد دارند.
کار با AnimatedVisibility: انیمیشن های ورود و خروج مولفه ها
رایجترین نیاز در توسعه برنامه هایی اندروید، ظاهر یا پنهان شدن یک المان بر اساس شرط بولین است. در رویکرد سنتی از ویزیبیلیتی استفاده میشد که پرش های ناگهانی ایجاد میکرد.
کامپوزبل AnimatedVisibility به صورت پیشفرض ترنزیشن های محو شدن یا همان fadeIn و بزرگ شدن را ترکیب میکند. شما میتوانید این رفتار را به طور کامل سفارشی سازی نمایید:
@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)
)
}
}
}
با عملگر + میتوان افکت های حرکتی گوناگون را با هم ترکیب کرد تا المان به شکل همزمان اسلاید و محو گردد.
ترکیب ترنزیشن ها و کالبدشکافی منحنی های حرکتی در فاز ورود و خروج
در هنگام اجرای انیمیشن های ورود و خروج، زیرسیستم گرافیکی کامپوز چندین فاز حرکتی مستقل را مدیریت مینماید. شکل زیر جریان پردازش این ترنزیشن ها را نشان میدهد:
ترکیب افکت های slideInHorizontally، expandVertically و scaleIn باعث روانتر شدن نمایش کارت های لیست و دیالوگ ها میشود.
هر کدام از این توابع دارای پارامتر animationSpec اختصاصی هستند که کنترل دقیق سرعت و تاخیر زمانی را مهیا میسازد.
انیمیشن اختصاصی فرزندان با AnimatedVisibilityScope و animateEnterExit
گاهی اوقات قصد دارید المان های درون یک کانتینر ترنزیشن های متفاوتی نسبت به والد خود داشته باشند. برای مثال عنوان کانتینر از چپ وارد شود در حالی که آیکون آن از راست اسلاید کند.
بدنه تابع AnimatedVisibility یک اسکوپ اختصاصی به نام AnimatedVisibilityScope فراهم میکند. به کمک این اسکوپ، مودیفایر Modifier.animateEnterExit() روی هر المان فرزند قابل استفاده است:
@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، هر تغییری در سایز فرزندان به صورت نرم انیمیت خواهد شد:
@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 بهترین گزینه است:
@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:برای جابجایی المان ها در محورهای مختصات دو بعدی.
@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: تغییر آنی و بدون انیمیشن برای موقعیت های فوری یا تنظیمات دسترسی پذیری.
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:حرکت ملایم در شروع و پایان، مناسب برای جابجایی دکمه ها و تغییر اندازه تب ها.
val customM3Curve = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1.0f)
val m3TweenSpec = tween(
durationMillis = 400,
easing = customM3Curve
)
ماشین حالت چندگانه با updateTransition برای هماهنگی ویژگی ها
هنگامی که یک المان وضعیت های گوناگونی دارد و در هر وضعیت چندین خصوصیت (رنگ، گوشه ها، سایز و چرخش) باید به صورت همزمان انیمیت شوند، استفاده مجزا از چند تابع مستقل باعث ناهماهنگی حرکتی میگردد.
در این شرایط، updateTransition به عنوان یک ماشین وضعیت جامع تمامی مقادیر را به صورت یکپارچه همگام میسازد:
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 از تایم لاین مشترک پیروی میکنند. شکل زیر نحوه هماهنگی متغیرها را نمایش میدهد:
علاوه بر این، در محیط اندروید استودیو ابزار Animation Preview مستقیما به شما اجازه میدهد فریم های انیمیشن را به صورت زنده دیباگ کنید.
شما میتوانید انیمیشن را فریم به فریم جلو برده و بررسی کنید که آیا در فواصل میانی تداخلی در چیدمان رخ میدهد یا خیر.
انیمیشن های تکرارشونده و افکت های مداوم با rememberInfiniteTransition
برای ایجاد انیمیشن های پیوسته مانند افکت لودینگ شیمر یا چرخش نشانگر بارگذاری، از تابع rememberInfiniteTransition استفاده میکنیم:
@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 در لایه دراو این افکت رندر میشود:
@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 به عنوان هسته سیستم انیمیشن، امکان کنترل دستی و اجرای تعلیقی توابع در قالب کروتین ها را مهیا میسازد.
این کلاس به شما اجازه میدهد انیمیشن های در حال اجرا را در صورت لمس مجدد کاربر متوقف ساخته و کنترل فیزیکی را بدون لگ به دست بگیرید.
پیاده سازی کد تعاملی Swipe to Dismiss با کلاس هسته Animatable
در کد زیر مشاهده میکنید که چطور با مودیفایر pointerInput مقادیر جابجایی انگشت را رصد کرده و در پایان با انیمیشن فنری بازمیگردانیم:
@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 را فراهم کرده است که همان الگوریتم فرود طبیعی اسکرول لیست های اندروید را پیاده سازی میکند:
@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 این قابلیت به صورت بومی در دسترس قرار گرفته است. با دادن کلید یکتا به دو المان در دو صفحه مجزا، انیمیشن انتقال اندازه و موقعیت به شکل خودکار محاسبه میگردد:
@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، میتوان مقیاس دکمه را به شکل زیر انیمیت کرد:
@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 میتوان فایل های وکتور متحرک را بارگذاری و بر اساس وضعیت تغییر داد:
@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، این افکت جذاب بدون افت فریم پیاده سازی میشود:
@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 در هر فریم دوباره اجرا شود که موجب افت شدید فریم و خالی شدن باتری میگردد.
برای دستیابی به عملکرد بدون لگ و تثبیت نرخ ۱۲۰ فریم بر ثانیه، همیشه باید خواندن مقدار را به فاز لایوت یا دراو موکول کنید:
// اشتباه: بازتولید مکرر در هر فریم
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 | ژست های لمسی درگ و فیزیک پرتاب |
اشتباهات رایج توسعه دهندگان اندروید در انیمیشن های کامپوز و راه حل ها
بررسی پروژه هایی واقعی نشان میدهد که رعایت ۴ اصل زیر مانع از بروز مشکلات رایج عملکردی میگردد:
- اجرای انیمیشن های بینهایت در تب های پسزمینه: استفاده از
rememberInfiniteTransitionدر کامپوننت های مخفی باعث مصرف مداوم باتری میشود. - استفاده از ترنزیشن های زمانی طولانی: به جای استفاده از مدت زمان های طولانی در
tween، همواره از فیزیک فنر برای ایجاد حرکات ارگونومیک بهره ببرید. - عدم درج برچسب در توابع انیمیشن: ننوشتن پارامتر
labelامکان دیباگ در تب انیمیشن اندروید استودیو را سلب میکند. - بازتولید اشیاء در بدنه: از تعریف نمونه های جدید در هر فریم اجتناب نموده و وضعیت ها را با
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 فعال شده و امکان بررسی تایم لاین فریم به فریم فراهم میگردد.
۱۱. چطور انیمیشن را بر اساس تنظیمات سیستم عامل کاربر غیرفعال کنیم؟
با بررسی مقدار مقیاس زمانی انیمیشن در سیستم عامل و قرار دادن زمان ترنزیشن ها بر روی صفر میلیثانیه در زمان فعال بودن گزینه کاهش انیمیشن.


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