شارك المقالة
Jetpack Compose Swipe Cards إنشاء بطاقات متحركة قابلة للسحب في iOS و Android

Jetpack Compose Swipe Cards إنشاء بطاقات متحركة قابلة للسحب في iOS و Android

Jetpack Compose Swipe Cards – إنشاء بطاقات متحركة قابلة للسحب في Android وiOS إذا كنت تعمل على تطبيق Android أو…

Jetpack Compose Swipe Cards – إنشاء بطاقات متحركة قابلة للسحب في Android وiOS

إذا كنت تعمل على تطبيق Android أو iOS وتريد إنشاء واجهة تحتوي على بطاقات متراكبة يمكن سحبها يمينًا أو يسارًا أو للأعلى والأسفل، مثل البطاقات الموجودة في تطبيقات التعارف أو المحافظ البنكية أو منصات مشاهدة الأفلام، فإن مكتبة SwipingCards توفر طريقة سهلة لتنفيذ ذلك باستخدام Kotlin وCompose Multiplatform.

تتميز المكتبة بأنها لا تقوم بحذف البطاقة عند سحبها، بل تنقلها إلى نهاية مجموعة البطاقات، مما يسمح للمستخدم بالاستمرار في تصفحها بشكل دائري، مع تأثيرات ثلاثية الأبعاد وحركات Spring Animation تمنح الواجهة مظهرًا سلسًا واحترافيًا.

في هذا المقال سنتعرف على طريقة استخدام مكتبة SwipingCards لإنشاء Jetpack Compose Swipe Cards، وكيفية تخصيص شكل البطاقات والتعامل مع اتجاهات السحب وعرض العناصر داخل تطبيقات Android وiOS.

ما هي مكتبة SwipingCards؟

SwipingCards هي مكتبة مفتوحة المصدر من تطوير Maxim Kachinkin، مخصصة لإنشاء مجموعة بطاقات متراكبة وقابلة للسحب داخل تطبيقات Compose Multiplatform.

تعتمد المكتبة على مبدأ Circular Card Deck، بحيث يتم نقل البطاقة الأولى إلى نهاية المجموعة عند سحبها بدلًا من حذفها نهائيًا.

كما تدعم المكتبة عرض البطاقات باستخدام أي Composable، ولذلك يمكنك إنشاء بطاقة تحتوي على صورة، أو بيانات حساب بنكي، أو معلومات منتج، أو حتى واجهة كاملة.

مميزات مكتبة SwipingCards

  • دعم Android وiOS من خلال Compose Multiplatform.
  • سحب البطاقات في أربعة اتجاهات.
  • إرجاع البطاقة المسحوبة إلى نهاية القائمة.
  • دعم عدد غير محدود من دورات تصفح البطاقات.
  • تأثير دوران ثلاثي الأبعاد 3D Rotation.
  • حركات Spring Animation.
  • تفاعل البطاقات الخلفية مع حركة البطاقة الأمامية.
  • دعم السحب السريع Fling Gestures.
  • Haptic Feedback عند تجاوز حد السحب.
  • إمكانية تحديد عدد البطاقات الظاهرة.
  • إمكانية التحكم في زاوية دوران البطاقة.
  • تخصيص شكل البطاقة بالكامل.
  • التعامل مع تغييرات القائمة الخارجية باستخدام Stable Keys.
  • إرجاع نتيجة السحب واتجاهه والعناصر بعد إعادة ترتيبها.

كيف تعمل SwipingCards؟

لنفترض أن لدينا خمس بطاقات مرتبة بالشكل التالي:

Card 1
Card 2
Card 3
Card 4
Card 5

عندما يسحب المستخدم البطاقة الأولى، يتم نقلها إلى نهاية القائمة:

Card 2
Card 3
Card 4
Card 5
Card 1

وعندما يسحب بطاقة أخرى تصبح القائمة:

Card 3
Card 4
Card 5
Card 1
Card 2

وهكذا يمكن للمستخدم تصفح البطاقات باستمرار دون الوصول إلى نهاية ثابتة للمجموعة.

تثبيت مكتبة SwipingCards

المكتبة متاحة من خلال Maven Central بالإصدار:

0.1.0

يمكن إضافتها إلى مشروع Compose Multiplatform داخل ملف build.gradle.kts:

kotlin {

    sourceSets {

        commonMain.dependencies {

            implementation(
                "io.github.makzimi:swipingcards:0.1.0"
            )

        }

    }

}

إذا كنت تستخدم مشروع Android فقط، يمكنك إضافة الاعتمادية داخل قسم dependencies في وحدة التطبيق، مع التأكد من إعداد مستودع mavenCentral().

dependencies {

    implementation(
        "io.github.makzimi:swipingcards:0.1.0"
    )

}

ملاحظة: يشير توثيق المكتبة إلى أن الحد الأدنى لإصدار Android المطلوب هو API 33، أي Android 13، لذلك يجب مراعاة ذلك قبل إضافتها إلى مشروع يدعم إصدارات أقدم.

إنشاء Model خاص بالبطاقات

قبل استخدام المكتبة نحتاج إلى إنشاء Model يمثل البيانات التي ستظهر داخل كل بطاقة.

data class CardItem(

    val id: Int,

    val title: String,

    val description: String,

    val color: Color

)

في هذا المثال تحتوي كل بطاقة على معرف فريد وعنوان ووصف ولون للخلفية.

يمكن إنشاء مجموعة من البطاقات:

val cards = listOf(

    CardItem(
        id = 1,
        title = "Card One",
        description = "First Card",
        color = Color(0xFF6366F1)
    ),

    CardItem(
        id = 2,
        title = "Card Two",
        description = "Second Card",
        color = Color(0xFF10B981)
    ),

    CardItem(
        id = 3,
        title = "Card Three",
        description = "Third Card",
        color = Color(0xFFF59E0B)
    )

)

إنشاء أول Swipe Cards باستخدام Jetpack Compose

بعد تجهيز البيانات يمكن استخدام Composable الرئيسية:

SwipingCards()

وتمرير قائمة البطاقات إليها:

@Composable
fun CardsScreen() {

    val cards = remember {
        listOf(
            CardItem(
                1,
                "Flutter",
                "Mobile Development",
                Color.Blue
            ),
            CardItem(
                2,
                "Kotlin",
                "Android Development",
                Color.Magenta
            ),
            CardItem(
                3,
                "Compose",
                "Modern Android UI",
                Color.Cyan
            )
        )
    }

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {

        SwipingCards(

            cards = cards,

            key = { it.id },

            modifier = Modifier
                .fillMaxWidth(0.8f)
                .aspectRatio(2f / 3f),

            maxVisibleCards = 3

        ) { item ->

            MyCard(item)

        }

    }
}

بهذا الشكل أنشأنا مجموعة من البطاقات القابلة للسحب، مع الاستفادة من الحركات والإيماءات التي توفرها المكتبة.

إنشاء تصميم مخصص للبطاقة

الميزة المهمة في SwipingCards أنها لا تفرض تصميمًا محددًا للبطاقة.

يمكن إنشاء Composable خاصة بنا:

@Composable
fun MyCard(
    item: CardItem
) {

    Box(

        modifier = Modifier
            .fillMaxSize()
            .clip(
                RoundedCornerShape(24.dp)
            )
            .background(item.color),

        contentAlignment = Alignment.Center

    ) {

        Column(
            horizontalAlignment =
                Alignment.CenterHorizontally
        ) {

            Text(
                text = item.title,
                color = Color.White,
                fontSize = 28.sp,
                fontWeight = FontWeight.Bold
            )

            Spacer(
                modifier = Modifier.height(12.dp)
            )

            Text(
                text = item.description,
                color = Color.White,
                fontSize = 16.sp
            )

        }

    }

}

ستقوم المكتبة بعرض هذه البطاقة داخل المجموعة والتعامل مع حركة السحب والدوران وترتيب البطاقات تلقائيًا.

ما وظيفة key في SwipingCards؟

من أهم الخصائص التي يجب تمريرها إلى المكتبة:

key = { it.id }

تُستخدم هذه القيمة للتعرف على هوية كل بطاقة، حتى إذا تغير ترتيب القائمة.

يجب أن تكون قيمة key ثابتة وفريدة لكل عنصر.

على سبيل المثال، لا يُفضل الاعتماد على موقع البطاقة داخل القائمة إذا كان ترتيب العناصر يتغير باستمرار.

لأن المكتبة تستخدم هذه المعرفات لمزامنة الترتيب الداخلي مع القائمة التي يوفرها التطبيق.

التحكم في عدد البطاقات الظاهرة

يمكن استخدام الخاصية:

maxVisibleCards

لتحديد عدد البطاقات التي تظهر في الواجهة في نفس الوقت.

مثلًا:

SwipingCards(

    cards = cards,

    key = { it.id },

    maxVisibleCards = 4

) { item ->

    MyCard(item)

}

هنا يتم عرض أربع بطاقات كحد أقصى داخل المجموعة المرئية.

أما البطاقات الإضافية، فتبقى ضمن قائمة داخلية غير ظاهرة، وتنتقل تدريجيًا إلى المقدمة عندما يتم سحب البطاقات الحالية.

تخصيص حجم البطاقات

لا تفرض المكتبة عرضًا أو ارتفاعًا ثابتًا، بل تعتمد على القيود التي نمررها من خلال:

modifier

لإنشاء بطاقة عمودية:

Modifier
    .fillMaxWidth(0.8f)
    .aspectRatio(2f / 3f)

أما لإنشاء بطاقة بنكية أفقية:

Modifier
    .fillMaxWidth(0.9f)
    .aspectRatio(1.586f)

وهذا يجعل المكتبة مناسبة لواجهات مختلفة دون الحاجة إلى تعديل منطق الحركة نفسه.

إضافة تأثير 3D Rotation

واحدة من أهم مميزات المكتبة وجود تأثير ثلاثي الأبعاد أثناء سحب البطاقة.

فعندما يبدأ المستخدم بتحريك البطاقة، فإنها لا تنتقل فقط أفقيًا أو عموديًا، بل تميل بمنظور ثلاثي الأبعاد.

يمكن التحكم في أقصى زاوية باستخدام:

maxRotationY

مثال:

SwipingCards(

    cards = cards,

    key = { it.id },

    maxRotationY = 45f

) { item ->

    MyCard(item)

}

القيمة الافتراضية في المكتبة هي:

38f

وكلما تغيرت القيمة يتغير مقدار الميلان الأقصى المسموح أثناء السحب.

تحديد المسافة المطلوبة لتنفيذ Swipe

يمكنك التحكم في المسافة التي يجب أن تقطعها البطاقة قبل اعتماد السحب.

وذلك باستخدام:

swipeThresholdFraction

القيمة الافتراضية:

0.20f

أي إن تجاوز إزاحة السحب نحو 20% من عرض مجموعة البطاقات يمكن أن يؤدي إلى اعتماد عملية السحب.

مثال:

SwipingCards(

    cards = cards,

    key = { it.id },

    swipeThresholdFraction = 0.25f

) { item ->

    MyCard(item)

}

هنا جعلنا المسافة المطلوبة تساوي 25% من عرض المجموعة.

كما تدعم المكتبة السحب السريع Fling، والذي يمكن أن يؤدي إلى اعتماد السحب بناءً على سرعة الحركة.

معرفة اتجاه السحب

توفر المكتبة Callback باسم:

onSwipe

يتم استدعاؤها بعد اعتماد عملية السحب.

SwipingCards(

    cards = cards,

    key = { it.id },

    onSwipe = { result ->

        println(
            result.direction
        )

    }

) { item ->

    MyCard(item)

}

يمكن أن تحتوي نتيجة الاتجاه على إحدى القيم التالية:

SwipeDirection.Left

SwipeDirection.Right

SwipeDirection.Up

SwipeDirection.Down

وبذلك يمكنك تنفيذ إجراءات مختلفة حسب الاتجاه.

تنفيذ عملية مختلفة لكل اتجاه

على سبيل المثال، لنفترض أنك تريد إنشاء واجهة مشابهة لتطبيقات التعارف.

يمكنك التعامل مع السحب بهذا الشكل:

onSwipe = { result ->

    when (result.direction) {

        SwipeDirection.Left -> {
            println("Dislike")
        }

        SwipeDirection.Right -> {
            println("Like")
        }

        SwipeDirection.Up -> {
            println("Super Like")
        }

        SwipeDirection.Down -> {
            println("Skip")
        }

    }

}

هذا السلوك من اختيار التطبيق؛ فالمكتبة نفسها لا تفرض معنى معينًا لكل اتجاه.

ما هو SwipeResult؟

عندما تنتهي عملية السحب تعيد المكتبة كائنًا من نوع:

SwipeResult<T>

ويحتوي على البيانات التالية:

data class SwipeResult<T>(

    val card: T,

    val key: Any,

    val direction: SwipeDirection,

    val resultingOrder: List<T>

)

كل قيمة لها وظيفة مختلفة.

card

يمثل العنصر الذي قام المستخدم بسحبه.

result.card

key

يمثل المعرف الفريد للبطاقة.

result.key

direction

يمثل اتجاه السحب.

result.direction

resultingOrder

يمثل ترتيب البطاقات بعد نقل البطاقة المسحوبة إلى نهاية المجموعة.

result.resultingOrder

هذه الخاصية مفيدة إذا أردت الاحتفاظ بترتيب البطاقات بعد تفاعل المستخدم معها.

حفظ ترتيب البطاقات بعد السحب

يمكنك الاحتفاظ بقائمة البطاقات داخل State وتحديثها عند انتهاء عملية السحب.

var cards by remember {

    mutableStateOf(initialCards)

}

SwipingCards(

    cards = cards,

    key = { it.id },

    onSwipe = { result ->

        cards = result.resultingOrder

    }

) { item ->

    MyCard(item)

}

الميزة المهمة أن SwipingCards تتعامل مع هذا التحديث باعتباره تأكيدًا للترتيب الجديد، ولا تحتاج إلى إعادة تشغيل الحركة من البداية.

هل يمكن إضافة أو حذف بطاقات أثناء التشغيل؟

نعم، تدعم المكتبة تغيير قائمة العناصر الخارجية.

إذا أضفت بطاقة جديدة أو حذفت بطاقة موجودة، تحاول المكتبة تحديث مجموعتها الداخلية مع الحفاظ على الحركات الجارية.

لكن إذا تم تغيير ترتيب العناصر الباقية خارجيًا، يتم إعادة وضعها حسب الترتيب الجديد دون Animation انتقالية لهذا التغيير الخارجي.

وهذا مفيد جدًا عند استخدام State Management أو تحديث البيانات من Backend.

ما هي Background Repulsion؟

من المؤثرات التي تقدمها المكتبة تأثير يسمى:

Background Repulsion

عندما يبدأ المستخدم بسحب البطاقة الأمامية، تتحرك البطاقات الموجودة خلفها بشكل تفاعلي بدل البقاء ثابتة تمامًا.

يعطي ذلك إحساسًا بأن جميع البطاقات مرتبطة بحركة واحدة، مما يجعل التجربة أكثر طبيعية.

Spring Animation في SwipingCards

تعتمد المكتبة على حركات Spring لإرجاع البطاقة إلى مكانها أو نقل البطاقات إلى مواقعها الجديدة.

إذا قام المستخدم بسحب البطاقة مسافة قصيرة دون الوصول إلى حد اعتماد السحب، فإنها تعود إلى مكانها بحركة ناعمة.

أما إذا اكتملت عملية السحب، فتتحرك البطاقة إلى الخارج ثم تنتقل إلى نهاية المجموعة مع تقدم البطاقة التالية.

دعم Haptic Feedback

توفر المكتبة اهتزازًا تفاعليًا عندما تصل حركة السحب إلى الحد المطلوب، وكذلك عند اعتماد السحب.

تساعد هذه الخاصية المستخدم على الشعور باستجابة الواجهة بدل الاعتماد على الحركة البصرية فقط.

يعتمد الإحساس الفعلي بالاهتزاز على الجهاز والمنصة وإعدادات المستخدم.

استخدام SwipingCards لعرض بطاقات بنكية

يمكن استخدام المكتبة لإنشاء محفظة تحتوي على مجموعة بطاقات دفع متراكبة.

مثلًا:

data class BankCard(

    val id: String,

    val cardHolder: String,

    val lastFourDigits: String,

    val color: Color

)

ثم عرض العناصر:

SwipingCards(

    cards = bankCards,

    key = { it.id },

    modifier = Modifier
        .fillMaxWidth(0.9f)
        .aspectRatio(1.586f),

    maxVisibleCards = 3,

    onSwipe = { result ->

        println(
            "Selected: ${result.card.id}"
        )

    }

) { card ->

    BankCardView(card)

}

وبذلك يستطيع المستخدم التنقل بين بطاقاته بسحب البطاقة الموجودة في المقدمة.

إنشاء تصميم بطاقة بنكية

يمكن بناء البطاقة باستخدام تدرج لوني وأيقونات ونصوص:

@Composable
fun BankCardView(card: BankCard) {

    Box(

        modifier = Modifier
            .fillMaxSize()
            .clip(
                RoundedCornerShape(20.dp)
            )
            .background(
                Brush.linearGradient(
                    colors = listOf(
                        card.color,
                        Color.Black
                    )
                )
            )
            .padding(24.dp)

    ) {

        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement =
                Arrangement.SpaceBetween
        ) {

            Text(
                text = "BANK CARD",
                color = Color.White,
                fontSize = 20.sp,
                fontWeight = FontWeight.Bold
            )

            Text(
                text =
                    "•••• •••• •••• ${card.lastFourDigits}",
                color = Color.White,
                fontSize = 22.sp
            )

            Text(
                text = card.cardHolder,
                color = Color.White
            )

        }

    }

}

هذا المثال يقدم تصميمًا مبسطًا ويمكن تطويره بإضافة شعار البنك وأيقونة الشريحة وأنواع البطاقات المختلفة.

استخدام SwipingCards في تطبيقات الأفلام

يمكن أيضًا استخدامها لإنشاء قائمة أفلام تشبه بطاقات منصات البث.

مثل:

data class Movie(

    val id: Int,

    val title: String,

    val posterUrl: String

)

ثم:

SwipingCards(

    cards = movies,

    key = { it.id },

    modifier = Modifier
        .fillMaxWidth(0.8f)
        .aspectRatio(2f / 3f),

    maxVisibleCards = 4

) { movie ->

    MoviePosterCard(movie)

}

يمكن تنفيذ MoviePosterCard باستخدام أي مكتبة عرض صور مناسبة لمشروعك، مثل Coil على Android.

ويمنح هذا الأسلوب المستخدم طريقة تفاعلية لتصفح الأفلام بدل الاعتماد على قائمة تقليدية فقط.

الأمثلة المتوفرة داخل المشروع

يوفر المستودع أربعة أمثلة رئيسية توضح استخدام المكتبة في واجهات مختلفة.

1. Dating Cards

مجموعة من خمس بطاقات عمودية بنسبة أبعاد 2:3، تحتوي على صور وبيانات وعناصر تفاعلية مناسبة لتطبيقات التعارف.

2. Bank Cards

مجموعة من ثلاث بطاقات بنكية أفقية بنسبة أبعاد 1.586:1، مع تأثيرات Gradient وعناصر البطاقة المالية.

3. D&D Cards

مجموعة من عشر بطاقات مستوحاة من ألعاب تقمص الأدوار، تتضمن صورًا وإحصائيات وعناصر مرئية مختلفة.

4. Streaming Cards

مجموعة من ست بطاقات لعرض محتوى ترفيهي وأفلام، تشبه ملصقات خدمات البث، مع أزرار مثل Play وMy List.

هيكل المشروع

يتكون المستودع من عدة Modules رئيسية:

SwipingCards/
│
├── swipingcards/
│
├── samples/
│   ├── shared/
│   ├── androidApp/
│   └── iosApp/
│
├── build.gradle.kts
│
├── settings.gradle.kts
│
└── README.md

المجلد swipingcards يحتوي على المكتبة، بينما يحتوي samples/shared على أمثلة الواجهات المشتركة بين Android وiOS.

تشغيل المشروع على Android

يمكن تنزيل المستودع باستخدام Git:

git clone https://github.com/makzimi/SwipingCards.git

cd SwipingCards

ثم فتح المشروع باستخدام Android Studio وتشغيل تطبيق الأمثلة، أو تنفيذ:

./gradlew :samples:androidApp:installDebug

يجب التأكد من إعداد Android SDK وتوصيل جهاز متوافق أو تشغيل Emulator مناسب.

تشغيل المشروع على iOS

يدعم المشروع أيضًا iOS باستخدام Compose Multiplatform.

بعد تنزيل المستودع يمكنك فتح:

samples/iosApp

باستخدام Xcode وتشغيل التطبيق على iOS Simulator.

ستظهر أمثلة الواجهات المشتركة نفسها الموجودة في إصدار Android.

هل SwipingCards مناسبة لتطبيقات Production؟

توفر المكتبة API واضحة ومجموعة من الخصائص التي تساعد على إنشاء واجهات حديثة، لكن يجب اختبارها قبل اعتمادها في تطبيق إنتاجي.

خصوصًا أن الإصدار المنشور حاليًا هو 0.1.0، ولذلك من الأفضل مراجعة حالة المشروع والتوافق مع إصدارات Kotlin وCompose Multiplatform المستخدمة لديك.

كما يجب اختبار الأداء، وسلوك اللمس، ودعم الوصول Accessibility، وتوافق الواجهة مع أحجام الشاشات المختلفة.

هل تعمل SwipingCards مع Flutter؟

لا تعمل هذه المكتبة مباشرة داخل Flutter، لأنها مكتوبة باستخدام Kotlin وCompose Multiplatform.

لكن يمكنك إنشاء تجربة مشابهة في Flutter باستخدام أدوات مثل:

GestureDetector

AnimationController

Transform

Stack

AnimatedBuilder

Tween

Matrix4

وتستطيع باستخدام هذه الأدوات بناء مجموعة بطاقات قابلة للسحب والدوران وتغيير الترتيب.

ترخيص مكتبة SwipingCards

المشروع مفتوح المصدر تحت ترخيص:

MIT License

يسمح هذا الترخيص باستخدام الكود وتعديله وتوزيعه، بما في ذلك داخل المشاريع التجارية، مع الالتزام بالاحتفاظ بإشعار حقوق النشر والترخيص المطلوب.

الخلاصة

تعتبر مكتبة SwipingCards خيارًا عمليًا لإنشاء واجهات Jetpack Compose Swipe Cards داخل تطبيقات Android وiOS باستخدام Compose Multiplatform.

تجمع المكتبة بين سهولة الاستخدام وحركات Spring وتأثيرات 3D Rotation ودعم الاتجاهات الأربعة، بالإضافة إلى إمكانية تخصيص المحتوى بالكامل وإعادة تدوير البطاقات بدل حذفها.

كما أن دعمها لعدد مختلف من البطاقات وتصميمات متعددة يجعلها مناسبة لتطبيقات التعارف والمحافظ الإلكترونية وتصفح المنتجات والأفلام وغيرها من التطبيقات التي تعتمد على التفاعل المباشر مع البطاقات.

وإذا كنت تريد التعرف على تقنيات تصميم واجهات Compose المتحركة، فإن دراسة الكود المصدري للمكتبة والأمثلة الموجودة داخل المستودع ستساعدك على فهم كيفية التعامل مع Gestures وAnimations وإدارة ترتيب العناصر.



الكود المصدري الكامل لمكتبة SwipingCards على GitHub

التوثيق الرسمي للتعامل مع Drag وSwipe وFling في Jetpack Compose

مزيد من المقالات : دمج Android Flavors مع Flutter: خطوات سهلة لربط تطبيقاتك
شاهد أيضًا
مقالات ذات صلة
🚫 مانع الإعلانات مفعل

يجب إيقاف مانع الإعلانات لاستكمال تصفح الموقع