إنشاء محفظة إلكترونية متحركة باستخدام Jetpack Compose وKotlin
عند تطوير تطبيقات Android، لم تعد الواجهات الحديثة تعتمد فقط على عرض البيانات والأزرار، بل أصبحت Animations والانتقالات التفاعلية جزءًا مهمًا من تجربة المستخدم.
تخيل أنك تعمل على تطبيق محفظة إلكترونية يحتوي على بطاقة بنكية يمكن تدويرها بشكل ثلاثي الأبعاد، وقائمة تحويلات تتمدد بالسحب، وصفحة ملف شخصي تظهر من خلف الشاشة الرئيسية بحركة تشبه لف الورق.
يمكن تنفيذ هذه التأثيرات باستخدام Jetpack Compose وKotlin دون الحاجة إلى استخدام Lottie أو مكتبات رسوم متحركة خارجية.
في هذا المقال سنتعرف على مشروع WalletAnimation الذي نشره المطور Kyriakos Georgiopoulos، ونشرح أهم التقنيات المستخدمة فيه وكيفية الاستفادة منها داخل تطبيقات Android.
ما هو مشروع WalletAnimation؟
WalletAnimation هو مثال برمجي لإنشاء واجهة محفظة إلكترونية متحركة باستخدام Jetpack Compose.
يتضمن المشروع عدة عناصر تفاعلية تجمع بين التصميم الحديث والحركة الفيزيائية، وأهمها:
- واجهة محفظة إلكترونية ذات تصميم حديث.
- بطاقة دفع يمكن تدويرها بتأثير 3D Flip.
- تأثير Shimmer متحرك على البطاقة.
- نافذة تحويلات قابلة للتوسيع بأسلوب Dynamic Island.
- التنقل بين الحالة المصغرة والموسعة عن طريق السحب.
- تأثير Roll Down لإظهار صفحة الملف الشخصي.
- انتقال متحرك لصورة الملف الشخصي.
- عرض الرصيد والمعاملات المالية.
- حركات Spring Animation.
- استخدام Gradients وGlass Effects.
التقنيات المستخدمة في المشروع
يعتمد المثال بشكل أساسي على مجموعة من APIs الموجودة داخل Jetpack Compose، من بينها:
Animatable
animateFloatAsState
rememberInfiniteTransition
AnchoredDraggableState
DraggableAnchors
graphicsLayer
drawWithContent
Brush
Canvas
LazyColumn
spring
tween
derivedStateOfكل واحدة من هذه الأدوات مسؤولة عن جزء معين من الحركة أو الرسم.
كيف تعمل واجهة المحفظة؟
تعتمد الفكرة الأساسية على وجود واجهتين فوق بعضهما:
Profile Screen
↑
|
Animated Wallet Screen
|
↓
Roll Down Animationتظهر شاشة المحفظة في المقدمة، بينما توجد شاشة الملف الشخصي خلفها.
عندما يضغط المستخدم على صورة الحساب، تتحرك شاشة المحفظة إلى الأسفل لتكشف عن صفحة الملف الشخصي الموجودة خلفها.
وخلال هذا الانتقال يتغير حجم صورة الحساب وموقعها تدريجيًا حتى تصبح في منتصف الشاشة.
إنشاء WalletAnimation
الدالة الرئيسية في المشروع هي:
@Composable
fun WalletAnimation() {
// Wallet UI
// Profile UI
// Animated transitions
}ويستخدم المطور BoxWithConstraints لتجميع العناصر وحساب أبعاد الشاشة.
@Composable
fun WalletAnimation() {
BoxWithConstraints(
modifier = Modifier.fillMaxSize()
) {
val screenWidth = maxWidth
val screenHeight = maxHeight
// Build wallet content
}
}ميزة BoxWithConstraints أنها تسمح بتحديد أحجام الحركة اعتمادًا على المساحة المتوفرة للواجهة.
إنشاء State للتحكم في حركة المحفظة
من أهم العناصر المستخدمة:
val screenRollOffset =
remember {
Animatable(0f)
}هذا المتغير مسؤول عن تحديد مقدار انتقال شاشة المحفظة عموديًا.
عندما تكون قيمته:
0fتظهر شاشة المحفظة في وضعها الطبيعي.
وعندما تزداد القيمة تتحرك الشاشة إلى الأسفل، مما يؤدي إلى ظهور شاشة الملف الشخصي.
إنشاء Roll Down Animation
يعتمد التأثير على Animatable وSpring Animation.
في الكود الأصلي يتم حساب مسافة الحركة بنسبة تقارب 75% من ارتفاع الشاشة:
val rollTargetPx = with(density) {
(screenHeightDp * 0.75f).toPx()
}وعند فتح الملف الشخصي تبدأ الحركة:
scope.launch {
screenRollOffset.animateTo(
targetValue = rollTargetPx,
animationSpec = spring(
dampingRatio = 0.75f,
stiffness = 160f
)
)
}وبالعكس، عند إغلاق الملف الشخصي تعود الشاشة إلى موضعها الأصلي:
scope.launch {
screenRollOffset.animateTo(
targetValue = 0f,
animationSpec = spring(
dampingRatio = 0.85f,
stiffness = 300f
)
)
}لماذا يستخدم Spring Animation؟
الحركة التقليدية باستخدام tween تعتمد غالبًا على الانتقال بين قيمتين خلال مدة ثابتة.
أما spring فتعطي إحساسًا طبيعيًا يعتمد على خصائص مثل التخميد والصلابة.
spring(
dampingRatio = 0.75f,
stiffness = 160f
)حيث تحدد:
- dampingRatio: مقدار الاهتزاز الذي يحدث قبل استقرار الحركة.
- stiffness: مدى سرعة استجابة الحركة للانتقال إلى القيمة المستهدفة.
ولهذا نلاحظ أن شاشة المحفظة لا تتحرك بشكل جامد، بل تبدو وكأنها تتحرك بكتلة ومرونة.
تحريك الشاشة باستخدام Modifier.offset
بعد حساب قيمة الحركة يتم تطبيقها على الواجهة باستخدام:
Modifier.offset {
IntOffset(
x = 0,
y = screenRollOffset.value.roundToInt()
)
}بذلك تصبح حركة الشاشة مرتبطة مباشرة بقيمة Animatable.
إنشاء تأثير لف الورق باستخدام drawWithContent
من التأثيرات البصرية المميزة في المشروع إضافة حافة أسطوانية متدرجة أثناء انتقال شاشة المحفظة.
يستخدم المطور:
Modifier.drawWithContent {
drawContent()
// Draw rolled edge effect
}وتقوم الفكرة على رسم المحتوى الطبيعي أولًا، ثم إضافة تدرجات لونية تمثل الظلال والإضاءة حول حافة الشاشة.
يتم حساب قطر الحافة باستخدام علاقة تتغير مع المسافة التي تحركتها الشاشة:
val cylinderDiameter =
(sqrt(rollPx) * 3.6f)
.coerceAtLeast(1f)ثم رسم تدرج لوني لإظهار الظل:
drawRect(
brush = Brush.verticalGradient(
colors = listOf(
Color.Black.copy(alpha = 0.6f),
Color.Transparent
)
),
size = Size(
size.width,
cylinderDiameter * 1.5f
)
)النتيجة هي إضافة إيحاء بصري بأن شاشة المحفظة تُلف إلى الأسفل بدل تحريك مستطيل مسطح فقط.
انتقال صورة الملف الشخصي Shared Avatar Animation
من العناصر المهمة في المشروع وجود صورة حساب تنتقل من مكانها الصغير داخل شاشة المحفظة إلى مكان أكبر في صفحة الملف الشخصي.
يتم تنفيذ ذلك داخل Composable باسم:
SharedProfileAvatar()بدلًا من إنشاء صورتين منفصلتين، يتم حساب الحجم والموضع اعتمادًا على تقدم الحركة.
في الكود الأصلي يبدأ حجم العنصر من:
52.dpويصل إلى:
100.dpعند فتح الملف الشخصي بالكامل.
استخدام lerp للتحريك بين قيمتين
تساعد الدالة lerp على حساب قيمة متوسطة بين البداية والنهاية اعتمادًا على تقدم الحركة.
val startSize = 52.dp
val endSize = 100.dp
val currentSize = lerp(
startSize,
endSize,
progress
)عندما تكون قيمة progress مساوية للصفر يظهر الحجم الأول، وعند وصولها إلى 1 يظهر الحجم النهائي.
وتستخدم الفكرة نفسها للتحكم في موقع الصورة:
val currentX = lerp(
startX,
endX,
progress
)
val currentY = lerp(
startY,
endY,
progress
)وبذلك يمكن تحريك عنصر واحد بين مكانين بسلاسة دون الحاجة إلى إعادة بناء واجهتين منفصلتين.
إنشاء Dynamic Island باستخدام Jetpack Compose
يتضمن المشروع عنصرًا تفاعليًا يشبه فكرة Dynamic Island، ولكنه مستخدم هنا كواجهة لإرسال الأموال.
يظهر العنصر في البداية على شكل شريط صغير أعلى الشاشة، ثم يستطيع المستخدم سحبه ليتحول إلى نافذة كبيرة تحتوي على قائمة مستلمين وزر متابعة.
يستخدم المشروع حالتين:
enum class BlobState {
Collapsed,
Expanded
}تمثل الحالة الأولى النافذة المصغرة، بينما تمثل الثانية النافذة الموسعة.
ما هو AnchoredDraggableState؟
توفر Compose أداة باسم:
AnchoredDraggableStateوهي مخصصة لإدارة العناصر القابلة للسحب بين مواضع أو قيم محددة مسبقًا.
في المشروع يتم استخدامها للتحكم في ارتفاع Dynamic Island أثناء السحب العمودي.
تحديد نقاط التوقف باستخدام DraggableAnchors
يتم تعريف نقطتين أساسيتين للحركة:
val anchors = DraggableAnchors {
BlobState.Collapsed at startHeightPx
BlobState.Expanded at endHeightPx
}عند سحب العنصر، تحدد Compose ما إذا كان ينبغي العودة إلى الحالة المصغرة أو التوسع إلى الحالة الكبيرة.
ويتم أخذ المسافة وسرعة الحركة بعين الاعتبار.
إنشاء AnchoredDraggableState
يستخدم الكود الأصلي الصيغة التالية في الإصدار الذي بُني عليه المثال:
val draggableState = remember {
AnchoredDraggableState(
initialValue = BlobState.Collapsed,
anchors = anchors,
positionalThreshold = {
it * 0.5f
},
velocityThreshold = {
with(density) {
100.dp.toPx()
}
},
snapAnimationSpec = spring(
dampingRatio = 0.85f,
stiffness = 250f
),
decayAnimationSpec =
decayAnimationSpec
)
}ملاحظة: API الخاصة بـ AnchoredDraggableState تطورت بين إصدارات Compose، لذلك قد تحتاج إلى تعديل طريقة التهيئة في الإصدارات الحديثة.
تفعيل السحب العمودي
بعد إعداد State يتم استخدام:
Modifier.anchoredDraggable(
state = draggableState,
orientation = Orientation.Vertical
)وبذلك يصبح العنصر قابلًا للسحب بين الحالتين.
كما يمكن إغلاقه برمجيًا باستخدام:
scope.launch {
draggableState.animateTo(
BlobState.Collapsed
)
}تحويل Dynamic Island إلى نافذة كاملة
أثناء سحب النافذة يتغير عرضها وارتفاعها ونصف قطر حوافها.
تبدأ الأبعاد في المثال من:
val startHeight = 56.dp
val startWidth = 220.dp
val startRadius = 28.dpثم تتوسع النافذة تدريجيًا حتى تقترب من عرض الشاشة وارتفاعها.
ويتم حساب الحجم اعتمادًا على تقدم الحركة:
val currentWidth = lerp(
startWidth,
endWidth,
widthProgress
)
val currentRadius = lerp(
startRadius,
endRadius,
widthProgress
)النتيجة هي تحول عنصر صغير إلى واجهة كبيرة دون قطع بصري مفاجئ.
إضافة Shadows وGlass Effects
لتحسين مظهر النافذة يستخدم المثال:
Modifier.graphicsLayer {
shape = RoundedCornerShape(currentRadius)
clip = true
shadowElevation = smoothProgress * 40f
}كما تُستخدم تدرجات لونية داكنة وحدود شفافة لإظهار تأثير قريب من Glass UI.
val GlassSurface =
Color.White.copy(alpha = 0.05f)مع إمكانية إضافة حد خفيف:
Modifier.border(
width = 1.dp,
color = Color.White.copy(alpha = 0.1f),
shape = RoundedCornerShape(16.dp)
)إنشاء بطاقة بنكية ثلاثية الأبعاد 3D Card Flip
من أجمل أجزاء المشروع وجود بطاقة بنكية يمكن تدويرها أفقيًا لإظهار الجهة الأمامية والخلفية.
الـ Composable المسؤول عن ذلك هو:
WetPaintCard()وتعتمد حركة البطاقة بشكل أساسي على:
rotationYداخل graphicsLayer.
استخدام rotationY لتدوير البطاقة
نبدأ بإنشاء قيمة لتحديد زاوية الدوران:
val rotation = remember {
Animatable(0f)
}ثم نطبق الزاوية على البطاقة:
Modifier.graphicsLayer {
rotationY = rotation.value
cameraDistance = 14f * density
}القيمة rotationY تحدد مقدار دوران العنصر حول المحور العمودي.
أما cameraDistance فتساعد على التحكم في منظور العرض الثلاثي الأبعاد.
إظهار الجهة الخلفية من البطاقة
يحسب المثال زاوية الدوران بعد تطبيعها إلى المجال من 0 إلى 360 درجة.
val normalizedAngle =
(rotation.value % 360 + 360) % 360
val isBackVisible =
normalizedAngle in 90f..270fإذا كانت زاوية الدوران في النطاق المحدد، يتم عرض الوجه الخلفي للبطاقة.
وبذلك يمكن بناء وجهين مختلفين وإظهار الوجه المناسب أثناء عملية التدوير.
تدوير البطاقة عن طريق السحب
لا يعتمد المشروع فقط على الضغط لتدوير البطاقة، بل يسمح للمستخدم بسحبها أفقيًا.
يستخدم:
Modifier.draggable(
state = rememberDraggableState { delta ->
scope.launch {
rotation.snapTo(
rotation.value + delta * 0.6f
)
}
},
orientation = Orientation.Horizontal
)هذا المثال مبسط؛ أما الكود الأصلي فيضيف عوامل مرتبطة باتجاه الدوران والقرب من منتصف الحركة، بالإضافة إلى تحديد الزاوية النهائية وفق سرعة السحب والمسافة.
إضافة تأثير Magnetic Rotation
في المشروع توجد فكرة مميزة تجعل دوران البطاقة يبدو وكأنها تنجذب إلى إحدى وضعياتها المستقرة.
عند اقتراب الدوران من الزوايا الوسطية تتغير استجابة الحركة، ثم تستقر البطاقة على الوجه الأمامي أو الخلفي عند انتهاء السحب.
يستخدم المطور قيمة تسمى:
magneticFactorلحساب مقدار استجابة زاوية الدوران أثناء السحب.
وهذا يجعل البطاقة أكثر واقعية من مجرد زيادة زاوية الدوران بمقدار ثابت.
إضافة Shimmer Animation للبطاقة
من التأثيرات الجميلة أيضًا وجود إضاءة متحركة على سطح البطاقة.
يستخدم المشروع:
rememberInfiniteTransition()لإنشاء حركة متكررة باستمرار.
val infiniteTransition =
rememberInfiniteTransition(
label = "Gloss"
)
val shimmerPhase by
infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 3500,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
),
label = "Shimmer"
)ثم يمكن استخدام قيمة shimmerPhase لتحريك موضع التدرج اللوني على البطاقة.
استخدام Brush لإنشاء تأثيرات لونية
يعتمد المثال على مجموعة من التدرجات اللونية لإنشاء الخلفيات والبطاقات والظلال.
مثل:
val IridescentBorder =
Brush.linearGradient(
colors = listOf(
Color(0xFF818CF8),
Color(0xFFC084FC),
Color.White.copy(alpha = 0.1f)
)
)تساعد هذه التدرجات على إنتاج حدود متعددة الألوان، مما يمنح البطاقة مظهرًا قريبًا من البطاقات المعدنية اللامعة.
إنشاء قسم الرصيد BalanceSection
تحت البطاقة يعرض المثال إجمالي الرصيد وبعض المعلومات المالية.
يتم ذلك باستخدام:
BalanceSection()ويعرض القسم عنوان الرصيد والمبلغ بالإضافة إلى مؤشر صغير للتغير النسبي.
كما يستخدم المشروع Shimmer Animation داخل مؤشر النسبة لإضافة حركة بسيطة دون تشتيت المستخدم.
إنشاء قائمة المعاملات المالية
يعرض المثال المعاملات الأخيرة باستخدام:
LazyColumnوهي مناسبة لعرض قوائم طويلة لأنها تنشئ العناصر المرئية حسب الحاجة.
يمكن بناء قائمة مشابهة بالشكل التالي:
@Composable
fun TransactionsList() {
val transactions = listOf(
"Shopping",
"Netflix",
"Electric Bill",
"Salary"
)
LazyColumn {
items(transactions.size) { index ->
Text(
text = transactions[index],
modifier = Modifier.padding(16.dp)
)
}
}
}وفي المشروع الأصلي يحتوي كل عنصر على أيقونة، ووصف المعاملة، والمبلغ، مع اختلاف طريقة العرض حسب ما إذا كانت العملية إضافة أموال أو خصمًا منها.
استخدام derivedStateOf لتحسين إدارة الحالة
من الأدوات المهمة المستخدمة في المشروع:
derivedStateOfيتم استخدامها لاشتقاق تقدم حركة المحفظة من قيمة الإزاحة.
val progress by remember {
derivedStateOf {
(
screenRollOffset.value /
rollTargetPx
).coerceIn(0f, 1f)
}
}وبذلك يمكن استخدام قيمة موحدة تتحرك بين 0 و1 للتحكم في عدة عناصر، مثل:
- حجم صورة الحساب.
- موضع صورة الحساب.
- شفافية صفحة الملف الشخصي.
- مقياس الصفحة.
- سمك حدود العناصر.
- شدة الظلال.
تغيير لون Status Bar أثناء الحركة
من التفاصيل التي اهتم بها المطور تغيير مظهر أيقونات Status Bar حسب تقدم الانتقال بين الشاشتين.
يستخدم:
WindowInsetsControllerCompatمع:
controller.isAppearanceLightStatusBars =
progress < 0.5fوبذلك تتغير ألوان أيقونات شريط الحالة لتتناسب مع الخلفية الحالية.
ما الفرق بين Animatable وanimateFloatAsState؟
يستخدم المشروع الأداتين في أماكن مختلفة.
Animatable
مناسبة عندما تحتاج إلى التحكم في الحركة بشكل مباشر، مثل بدء الحركة أو إيقافها أو تغيير هدفها داخل Coroutine.
val position = remember {
Animatable(0f)
}
scope.launch {
position.animateTo(
300f,
spring()
)
}animateFloatAsState
مناسبة عندما تريد جعل قيمة معينة تتحرك تلقائيًا كلما تغير الهدف الخاص بها.
val animatedProgress by
animateFloatAsState(
targetValue = progress,
label = "Progress"
)وهذا الأسلوب مفيد في تحريك الأبعاد والشفافية والقيم المرئية الأخرى.
كيف أستخدم المشروع داخل تطبيق Android؟
يمكن الاستفادة من المثال باتباع الخطوات التالية:
- إنشاء مشروع Android باستخدام Kotlin.
- تفعيل Jetpack Compose.
- إضافة مكتبات Material 3 وCompose Animation وFoundation.
- فتح ملف WalletAnimation.kt الموجود على GitHub Gist.
- نسخ الكود إلى ملف Kotlin داخل مشروعك.
- إضافة Package Name الخاص بمشروعك.
- مراجعة توافق AnchoredDraggableState مع إصدار Compose المستخدم.
- استدعاء WalletAnimation داخل Activity.
مثال:
class MainActivity : ComponentActivity() {
override fun onCreate(
savedInstanceState: Bundle?
) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
WalletAnimation()
}
}
}
}هل يحتاج المشروع إلى مكتبات Animation خارجية؟
لا يحتاج هذا المثال إلى Lottie أو Rive لتشغيل الحركات المذكورة.
فمعظم التأثيرات مبنية باستخدام واجهات Jetpack Compose الخاصة بالحركة والرسم والتفاعل مع المستخدم.
لكنه يعتمد بطبيعة الحال على مكتبات Compose وAndroidX اللازمة للمشروع.
هل يمكن استخدام الكود مباشرة في Production؟
يمكن الاستفادة من تصميم المشروع وتقنيات الحركة داخله، لكن يجب معرفة أنه UI Demo وليس تطبيق محفظة مالية متكاملًا.
الأرصدة والمعاملات والمستلمون الظاهرون في المثال بيانات توضيحية موجودة داخل الكود، وبعض الأزرار لا تنفذ عمليات مالية فعلية.
لذلك عند دمجه داخل تطبيق حقيقي ستحتاج إلى إضافة:
- Backend API.
- Authentication.
- State Management.
- إدارة بيانات المعاملات المالية.
- التحقق من صحة العمليات.
- حالات التحميل والأخطاء.
- اعتبارات Accessibility.
- اختبارات الأداء على مختلف الأجهزة.
تحسين أداء Animation في Jetpack Compose
إذا كنت تنوي استخدام تأثيرات مشابهة داخل مشروع كبير، فمن الأفضل مراعاة مجموعة من النقاط.
- استخدام remember للاحتفاظ بقيم الحركة بين عمليات Recomposition.
- استخدام derivedStateOf للقيم المشتقة عندما يساعد على تقليل التحديثات غير الضرورية.
- تجنب العمليات الحسابية الثقيلة أثناء رسم كل Frame.
- استخدام LazyColumn بدل إنشاء جميع عناصر القوائم دفعة واحدة.
- تقليل الرسوميات والظلال الثقيلة على الأجهزة محدودة الأداء.
- اختبار حركة السحب السريع والإغلاق المتكرر.
- مراعاة إعدادات تقليل الحركة عند تصميم التجربة النهائية.
هل يمكن إنشاء التأثير نفسه في Flutter؟
نعم، يمكن تنفيذ واجهة مشابهة باستخدام Flutter، ولكن لا يمكن نسخ كود Jetpack Compose مباشرة.
يمكن الاستفادة من أدوات Flutter مثل:
AnimationController
AnimatedBuilder
Transform
GestureDetector
CustomPainter
Stack
TweenAnimationBuilder
AnimatedPositioned
DraggableScrollableSheetلإنشاء البطاقة المتحركة والنافذة القابلة للتوسيع وانتقال الملف الشخصي.
لكن المثال الأصلي مكتوب باستخدام Kotlin ومخصص لـ Android وJetpack Compose.
ترخيص الكود المصدري
ملف WalletAnimation.kt منشور تحت ترخيص Apache License 2.0.
يسمح الترخيص بالاستخدام والتعديل والتوزيع ضمن شروطه، بما فيها الاحتفاظ بإشعارات الحقوق والترخيص المطلوبة.
يمكنك مراجعة إشعار الترخيص الموجود في بداية الملف الأصلي قبل استخدامه داخل مشروع تجاري.
الخلاصة
يقدم مشروع WalletAnimation مثالًا مميزًا على قوة Jetpack Compose Animations في إنشاء واجهات Android تفاعلية ومتقدمة.
فهو يجمع بين بطاقة بنكية قابلة للدوران بشكل ثلاثي الأبعاد، وتأثيرات Shimmer، وقائمة قابلة للتمدد بأسلوب Dynamic Island، وانتقال مرن بين شاشة المحفظة والملف الشخصي.
كما يوضح أهمية استخدام أدوات مثل Animatable وspring وAnchoredDraggableState وgraphicsLayer لبناء تجارب تفاعلية أكثر واقعية دون الاعتماد على ملفات Animation جاهزة.
إذا كنت مطور Android باستخدام Kotlin، فإن دراسة هذا المشروع ستساعدك على فهم مجموعة من أهم تقنيات الحركة والرسم المتقدمة داخل Jetpack Compose.
الكود المصدري الكامل لمشروع WalletAnimation على GitHub Gist
التوثيق الرسمي للحركات في Jetpack Compose
مزيد من المقالات : تفعيل الإشعارات (Notification flutter ios) عبر الشهادات والمفاتيح ودمجها مع Firebase





