شارك المقالة

إنشاء فقاعة صابون واقعية في Android باستخدام Jetpack Jetpack Compose AGSL

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

يمكن تنفيذ هذا التأثير باستخدام Jetpack Compose وAGSL Shader دون الحاجة إلى استخدام محرك ألعاب خارجي.

في هذا المقال سنتعرف على مثال مفتوح المصدر باسم PhysicsBubbleScreen من تطوير Kyriakos Georgiopoulos، والذي يجمع بين الفيزياء والرسوميات المتقدمة لإنشاء فقاعة صابون تفاعلية داخل تطبيقات Android باستخدام Kotlin.

ما هو PhysicsBubbleScreen؟

PhysicsBubbleScreen هو مثال برمجي لإنشاء فقاعة صابون واقعية باستخدام Jetpack Compose، مع الاستفادة من Android Graphics Shading Language (AGSL) لإنشاء التأثيرات البصرية.

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

يمكنك الاطلاع على الكود الكامل من خلال GitHub Gist الموجود أسفل المقال.

ما الذي يميز هذا التأثير؟

  • إنشاء فقاعة صابون بتأثيرات بصرية واقعية.
  • سحب الفقاعة باستخدام الإصبع.
  • تغيير حجم الفقاعة أثناء الحركة.
  • محاكاة تمدد الفقاعة وانكماشها حسب سرعة السحب.
  • ألوان قوس قزح الناتجة عن Thin-Film Interference.
  • انكسار وتشويه الخلفية خلف الفقاعة.
  • تأثير Chromatic Aberration.
  • استخدام Spring Animation للحركة الطبيعية.
  • انفجار الفقاعة عند النقر عليها ثم ظهورها مجددًا.
  • دعم Light Mode وDark Mode.
  • تأثير Circular Reveal أثناء تغيير الثيم.
  • توفير شكل بديل للأجهزة التي لا تدعم AGSL.

ما هو AGSL في Android؟

اختصار AGSL يعني:

Android Graphics Shading Language

وهي لغة مخصصة لكتابة Runtime Shaders على Android، وتسمح بتنفيذ عمليات حسابية ورسومية على الألوان والبكسلات لإنشاء تأثيرات متقدمة.

باستخدام AGSL يمكن تنفيذ تأثيرات مثل:

  • Glass Effects.
  • Liquid Animations.
  • Color Distortion.
  • Image Refraction.
  • Animated Gradients.
  • Custom Lighting Effects.
  • Interactive Visual Effects.

وهذا يجعلها مفيدة جدًا عند تطوير واجهات Android الحديثة.

هل AGSL مدعومة على جميع أجهزة Android؟

لا، حيث إن استخدام RuntimeShader في هذا المثال يتطلب Android 13 أو أحدث (API 33+).

أما الأجهزة الأقدم، فيستخدم المثال رسمًا بديلًا للفقاعة باستخدام RadialGradient بدل التأثير الرسومي الكامل.

if (Build.VERSION.SDK_INT >=
    Build.VERSION_CODES.TIRAMISU) {

    // Use AGSL Shader

} else {

    // Draw fallback bubble

}

وبذلك يمكن تشغيل الشاشة على الأجهزة القديمة، ولكن دون جميع تفاصيل التأثير البصري الموجود في Shader.

كيف تعمل الفقاعة؟

الفكرة الأساسية تعتمد على عدة مراحل تعمل معًا:

User Touch
     ↓
Drag Gestures
     ↓
Bubble Position
     ↓
Velocity Calculation
     ↓
Spring Deformation
     ↓
AGSL RuntimeShader
     ↓
Realistic Soap Bubble

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

إنشاء شاشة باستخدام Jetpack Compose

نبدأ بإنشاء Composable تستقبل أبعاد الشاشة وتستخدمها لحساب موضع الفقاعة.

@Composable
fun PhysicsBubbleScreen() {

    BoxWithConstraints(
        modifier = Modifier.fillMaxSize()
    ) {

        val screenWidth =
            constraints.maxWidth.toFloat()

        val screenHeight =
            constraints.maxHeight.toFloat()

        // Initialize bubble state here

    }
}

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

إنشاء State خاص بالفقاعة

من أهم أجزاء المثال وجود Class باسم:

PhysicsBubbleState

هذا الـ Class مسؤول عن إدارة معلومات الحركة والتأثيرات.

من أهم القيم الموجودة فيه:

val bubblePos

val deformationAnim

val popAnim

val themeRevealProgress

val shaderTime

كل قيمة مسؤولة عن جزء من التأثير.

bubblePos

تحدد مكان الفقاعة الحالي داخل الشاشة.

deformationAnim

تحدد مقدار تمدد أو انكماش الفقاعة.

popAnim

تحدد تقدم تأثير انفجار الفقاعة واختفائها.

themeRevealProgress

تستخدم لإنشاء انتقال دائري متحرك عند تغيير الثيم.

shaderTime

تمثل الوقت المستخدم لتحريك التأثيرات البصرية داخل Shader.

استخدام Animatable في Jetpack Compose

يعتمد المثال على Animatable للتحكم في موضع الفقاعة والتشوه والحركات الأخرى.

مثال مبسط لإنشاء موضع متحرك:

val bubblePosition = remember {

    Animatable(
        Offset(200f, 500f),
        Offset.VectorConverter
    )

}

ولتحريك الفقاعة إلى موقع جديد:

scope.launch {

    bubblePosition.animateTo(
        targetValue = Offset(
            200f,
            150f
        ),
        animationSpec = spring(
            dampingRatio = 0.65f,
            stiffness = Spring.StiffnessLow
        )
    )

}

استخدام Spring Animation يمنح الحركة إحساسًا طبيعيًا، بدل الانتقال المباشر من موقع إلى آخر.

التحكم في الفقاعة باستخدام Drag Gestures

يمكن اكتشاف حركة إصبع المستخدم باستخدام:

detectDragGestures

مثال توضيحي مبسط:

Modifier.pointerInput(Unit) {

    detectDragGestures { change, dragAmount ->

        change.consume()

        scope.launch {

            bubblePosition.snapTo(
                bubblePosition.value + dragAmount
            )

        }

    }

}

هنا نستخدم dragAmount لمعرفة مقدار الحركة الجديدة ثم تحديث موضع الفقاعة.

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

تغيير حجم الفقاعة أثناء السحب

واحدة من الحركات المميزة في الكود هي أن حجم الفقاعة يتغير تدريجيًا أثناء انتقالها من أسفل الشاشة إلى أعلاها.

الإعدادات الأصلية تتضمن:

val MAX_ORB_RADIUS = 250.dp

val MIN_ORB_RADIUS = 85.dp

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

يمكن حساب الحجم باستخدام lerp:

val currentRadius = lerp(
    maxRadius,
    minRadius,
    progress
)

حيث تتراوح progress بين 0 و1.

إضافة حركة واقعية باستخدام Spring Physics

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

لكن في المثال الأصلي يتم استخدام نظام يحاكي تأثير النوابض لتوليد تشوه ناعم أثناء الحركة.

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

وعندما تتوقف، لا تعود إلى الشكل الدائري مباشرة، بل تهتز تدريجيًا حتى تستقر.

هذا السلوك يجعلها تشبه فقاعة حقيقية تتحرك في الهواء.

ما هي Kinematic Spring Deformation؟

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

في المثال يتم حساب سرعة الفقاعة خلال كل Frame، ثم استخدامها لتحديث قيم التشوه.

كما يستخدم المطور حلقة تحديث تعتمد على:

withFrameNanos

وذلك للحصول على توقيت الإطارات بدل الاعتماد على Delay ثابت.

وتقوم الحلقة بتقليل مقدار الزمن المستخدم في الحساب إذا حدث بطء مفاجئ أثناء الرسم، حتى لا تصبح الحركة غير مستقرة.

ما هي Thin-Film Interference؟

ربما لاحظت أن فقاعات الصابون الحقيقية تظهر عليها ألوان متغيرة تشبه قوس قزح.

هذا التأثير يسمى:

Thin-Film Interference

ويحدث بسبب انعكاس الضوء عن السطحين الخارجي والداخلي لطبقة الصابون الرقيقة.

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

في الكود الأصلي تتم محاكاة هذا التأثير باستخدام AGSL مع حسابات تعتمد على سمك الغشاء وزاوية الضوء وأطوال موجية مختلفة للألوان الأساسية.

ما هي Fresnel Reflectance؟

هناك تأثير آخر مستخدم في المثال يسمى:

Fresnel Reflectance

وهو يوضح كيف تختلف شدة انعكاس الضوء حسب زاوية النظر إلى السطح.

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

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

R = R0 + (1 - R0) * pow(1 - cosTheta, 5)

هذه المعادلة تعرف باسم Schlick Approximation، وتستخدم لتقريب تغير الانعكاس بحسب زاوية الرؤية.

ما هو Chromatic Aberration؟

من التأثيرات المستخدمة أيضًا:

Chromatic Aberration

وهو تأثير ينتج عن اختلاف مقدار انكسار الضوء باختلاف الطول الموجي.

في الرسوميات يمكن محاكاته بأخذ عينات لونية من مواقع متقاربة ولكن مختلفة قليلًا لقنوات:

Red

Green

Blue

والنتيجة ظهور حواف لونية خفيفة بالقرب من الأجزاء المنكسرة، مما يضيف إحساسًا واقعيًا للفقاعة.

إنشاء RuntimeShader في Android

لتفعيل AGSL على Android 13 وما بعده، نستخدم:

RuntimeShader

مثال توضيحي:

val shader = remember {

    if (
        Build.VERSION.SDK_INT >=
        Build.VERSION_CODES.TIRAMISU
    ) {

        RuntimeShader(
            KINEMATIC_LENS_SHADER
        )

    } else {

        null

    }

}

المتغير KINEMATIC_LENS_SHADER يحتوي على كود AGSL المسؤول عن إنشاء التأثير البصري الكامل.

ولأن Shader الأصلي طويل ويحتوي على عدد من الحسابات الفيزيائية، من الأفضل الحصول عليه مباشرة من ملف المشروع بدل نسخه بشكل جزئي.

تمرير بيانات الفقاعة إلى Shader

يمكن إرسال القيم المتغيرة من Kotlin إلى AGSL باستخدام:

setFloatUniform()

مثلًا:

shader.setFloatUniform(
    "touchCenter",
    bubblePosition.x,
    bubblePosition.y
)

shader.setFloatUniform(
    "radius",
    bubbleRadius
)

shader.setFloatUniform(
    "progress",
    progress
)

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

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

تطبيق Shader على Jetpack Compose

من أهم الخطوات تحويل RuntimeShader إلى RenderEffect وتطبيقه على Graphics Layer.

مثال مبسط:

Modifier.graphicsLayer {

    if (
        Build.VERSION.SDK_INT >=
        Build.VERSION_CODES.TIRAMISU
        && shader != null
    ) {

        renderEffect =
            RenderEffect
                .createRuntimeShaderEffect(
                    shader,
                    "composable"
                )
                .asComposeRenderEffect()

    }

}

اسم composable هنا هو اسم Shader Input داخل كود AGSL الأصلي، والذي يسمح للتأثير بمعالجة المحتوى المرسوم.

إنشاء تأثير انفجار الفقاعة

المثال يدعم الضغط على الشاشة لتفجير الفقاعة.

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

detectTapGestures

وعند حدوث اللمس يتم تشغيل Animation لتغيير قيمة popAnim تدريجيًا.

مثال مبسط:

scope.launch {

    popAnim.animateTo(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = 150
        )
    )

    delay(2000)

    popAnim.snapTo(0f)

}

في الكود الأصلي، تتم أيضًا إعادة الفقاعة إلى موقعها السفلي بعد انتهاء مدة الاختفاء.

إضافة Light Mode وDark Mode

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

يتم حفظ حالة الثيم باستخدام:

var isDarkTheme by remember {
    mutableStateOf(false)
}

وعند تغيير الحالة، تتغير ألوان الخلفية والنصوص.

كما يستخدم المثال Circular Reveal لتوسيع دائرة من مكان زر التبديل حتى تغطي الشاشة بالكامل بالثيم الجديد.

ويتم تحريك ألوان النصوص باستخدام:

animateColorAsState()

وهذا يمنح الانتقال بين الثيمين مظهرًا أكثر سلاسة من التغيير الفوري.

دعم الأجهزة القديمة باستخدام Fallback

إذا كان الجهاز يعمل بنظام أقدم من Android 13، فلن يتم تنفيذ الـ RuntimeShader المستخدم في المثال.

بدلًا من ذلك يتم رسم فقاعة أبسط باستخدام Gradient.

if (
    Build.VERSION.SDK_INT <
    Build.VERSION_CODES.TIRAMISU
) {

    BubbleFallback(state)

}

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

كيف أستخدم المشروع داخل تطبيق Android؟

للاستفادة من المثال يمكنك اتباع الخطوات التالية:

  • إنشاء مشروع Android باستخدام Kotlin وJetpack Compose.
  • التأكد من إعداد Compose وMaterial 3 والاعتماديات المطلوبة.
  • فتح ملف PhysicsBubbleScreen.kt من GitHub Gist.
  • نسخ الملف وإضافته إلى مشروعك.
  • تغيير Package Name الموجود في أعلى الملف بما يناسب مشروعك.
  • مراجعة Imports وإصدارات المكتبات المستخدمة.
  • استدعاء Composable داخل Activity الخاصة بالتطبيق.

يمكن استدعاء الشاشة بالشكل التالي:

class MainActivity : ComponentActivity() {

    override fun onCreate(
        savedInstanceState: Bundle?
    ) {

        super.onCreate(savedInstanceState)

        setContent {

            MaterialTheme {

                PhysicsBubbleScreen()

            }

        }
    }
}

هذا المثال يوضح نقطة الدخول فقط؛ أما التأثير الكامل فيعتمد على وجود بقية Classes والدوال وShader الواردة في ملف Gist الأصلي.

هل يحتاج المشروع إلى مكتبة Physics خارجية؟

لا يعتمد هذا المثال على محرك فيزياء خارجي مثل Box2D.

بدلًا من ذلك يستخدم مجموعة من العمليات الحسابية مع Animation APIs المتوفرة في Jetpack Compose وAGSL.

لكن هذا لا يعني أنه يعمل بدون أي Dependencies، إذ يعتمد على مكتبات AndroidX وCompose وMaterial 3 وغيرها من مكونات Android المعتادة.

كيف أحسن الأداء؟

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

من الممارسات المفيدة:

  • عدم إنشاء Shader جديد مع كل Recomposition.
  • استخدام remember للاحتفاظ بالكائنات التي يمكن إعادة استخدامها.
  • تقليل إنشاء Objects جديدة داخل حلقات الرسم.
  • تقليل العمليات الحسابية غير الضرورية أثناء كل Frame.
  • استخدام withFrameNanos عند الحاجة إلى تحديثات متزامنة مع الإطارات.
  • اختبار الأداء على أجهزة Android متوسطة وضعيفة.
  • توفير Fallback للأجهزة التي لا تدعم RuntimeShader.

وقد اهتم المطور ببعض هذه النقاط داخل المثال، ومنها إعادة استخدام كائنات Path، والاحتفاظ بـ RuntimeShader، وإدارة تشوه الفقاعة من خلال حلقة تحديث مخصصة.

أين يمكن استخدام هذه التأثيرات؟

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

  • Interactive Onboarding.
  • Liquid UI Effects.
  • Animated Backgrounds.
  • Interactive Buttons.
  • Music Visualization.
  • Creative Loading Animations.
  • Glass and Refraction Effects.
  • واجهات التطبيقات الإبداعية والتجريبية.

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

هل يمكن تنفيذ هذا التأثير في Flutter؟

نعم، يمكن بناء تأثير مشابه في Flutter باستخدام تقنيات مثل FragmentShader وCustomPainter وGestureDetector، ولكن الكود الموجود في هذا المشروع مكتوب باستخدام Kotlin وJetpack Compose ولا يمكن نسخه مباشرة إلى Flutter.

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

هل يمكن استخدام الكود في مشروع تجاري؟

الملف الأصلي منشور مع إشعار ترخيص Apache License 2.0.

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

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

الخلاصة

يوضح مثال PhysicsBubbleScreen كيف يمكن الجمع بين Jetpack Compose وAGSL RuntimeShader والحسابات الفيزيائية لإنشاء رسوميات تفاعلية متقدمة داخل تطبيقات Android.

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

ويعد هذا المثال مصدرًا تعليميًا مفيدًا لأي مطور Kotlin أو Android يريد التعمق في Custom Graphics وShaders وPhysics-Based Animations داخل Jetpack Compose.


الكود المصدري الكامل لمشروع PhysicsBubbleScreen على GitHub Gist

التوثيق الرسمي للغة AGSL في Android


مزيد من المقالات : شرح مكتبة dartdosh في Flutter لأتمتة عملية Build وإدارة الإصدارات
شاهد أيضًا
مقالات ذات صلة
🚫 مانع الإعلانات مفعل

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