إضافة ظلال في Compose

تُبرز الظلال واجهة المستخدم بشكل مرئي، وتوضّح للمستخدمين إمكانية التفاعل مع العناصر، وتقدّم ملاحظات فورية حول إجراءاتهم. توفّر أداة Compose عدة طرق لدمج الظلال في تطبيقك:

  • ‫Modifier.shadow(): تنشئ هذه السمة ظلاً يستند إلى الارتفاع خلف دالة مركّبة يتوافق مع إرشادات "التصميم المتعدد الأبعاد".
  • ‫Modifier.dropShadow(): تنشئ هذه السمة ظلاً قابلاً للتخصيص يظهر خلف دالة مركّبة، ما يجعلها تبدو مرتفعة.
  • Modifier.innerShadow(): تنشئ هذه السمة ظلاً داخل حدود دالة مركّبة، ما يجعلها تبدو مضغوطة على السطح خلفها.

يُعدّ Modifier.shadow() مناسبًا لإنشاء ظلال أساسية، بينما يوفّر المعدّلان dropShadow() وinnerShadow() تحكّمًا أكثر دقة في عرض الظلال.

توضّح هذه الصفحة كيفية تنفيذ كل من هذه المُعدِّلات، بما في ذلك كيفية تحريك الظلال عند تفاعل المستخدم وكيفية ربط المُعدِّلَين innerShadow() وdropShadow() لإنشاء ظلال متدرّجة وظلال نيومورفية والمزيد.

إنشاء ظلال أساسية

تنشئ السمة Modifier.shadow() ظلاً أساسيًا يتّبع إرشادات التصميم المتعدد الأبعاد يحاكي مصدر ضوء علوي. يعتمد عمق الظل على قيمة elevation، ويتم اقتصاص الظل المسقط على شكل الدالة المركّبة.

@Composable
fun ElevationBasedShadow() {
    Box(
        modifier = Modifier.aspectRatio(1f).fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Box(
            Modifier
                .size(100.dp, 100.dp)
                .shadow(10.dp, RectangleShape)
                .background(Color.White)
        )
    }
}

ظل رمادي يحيط بشكل مستطيل أبيض
الشكل 1. ظل يستند إلى الارتفاع تم إنشاؤه باستخدام Modifier.shadow().

تنفيذ تظليل القطرات

استخدِم المُعدِّل dropShadow() لرسم ظل دقيق خلف المحتوى، ما يجعل العنصر يبدو مرتفعًا.

يمكنك التحكّم في الجوانب الرئيسية التالية من خلال مَعلمة Shadow:

  • ‫radius: تحدّد هذه السمة نعومة التمويه وانتشاره.
  • ‫color: تحدّد لون التظليل.
  • ‫offset: لتحديد موضع شكل الظل على طول المحورين "س" و"ص".
  • ‫spread: تتحكّم في توسيع أو تقليص هندسة الظل.

بالإضافة إلى ذلك، تحدّد المَعلمة shape الشكل العام للظل. ويمكنه استخدام أي شكل هندسي من حزمة androidx.compose.foundation.shape، بالإضافة إلى أشكال Material Expressive.

لتطبيق تظليل قطرات أساسي، أضِف المُعدِّل dropShadow() إلى سلسلة الدوال المركّبة، مع تحديد نصف القطر واللون والانتشار. يُرجى العِلم أنّ الخلفية purpleColor التي تظهر أعلى الظل يتم رسمها بعد المُعدِّل dropShadow():

@Composable
fun SimpleDropShadowUsage() {
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .width(300.dp)
                .height(300.dp)
                .dropShadow(
                    shape = RoundedCornerShape(20.dp),
                    shadow = Shadow(
                        radius = 10.dp,
                        spread = 6.dp,
                        color = Color(0x40000000),
                        offset = DpOffset(x = 4.dp, 4.dp)
                    )
                )
                .align(Alignment.Center)
                .background(
                    color = Color.White,
                    shape = RoundedCornerShape(20.dp)
                )
        ) {
            Text(
                "Drop Shadow",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 32.sp
            )
        }
    }
}

نقاط رئيسية حول الرمز

  • يتم تطبيق المُعدِّل dropShadow() على Box الداخلي. يتميّز الظل بالخصائص التالية:
    • شكل مستطيل مستدير الزوايا (RoundedCornerShape(20.dp))
    • نصف قطر تمويه يبلغ 10.dp، ما يجعل الحواف ناعمة وموزّعة
    • انتشار بقيمة 6.dp، ما يؤدي إلى توسيع حجم الظل وجعله أكبر من المربّع الذي يلقيه
    • قيمة ألفا تبلغ 0.5f، ما يجعل الظل شبه شفاف
  • بعد تحديد الظل،تم تطبيق معدِّل background().
    • يتم ملء Box باللون الأبيض.
    • يتم اقتصاص الخلفية إلى شكل المستطيل نفسه ذي الزوايا الدائرية مثل الظل.

النتيجة

تظليل قطرات رمادي حول شكل مستطيل أبيض
الشكل 2. تظليل القطرات حول الشكل

تنفيذ الظلال الداخلية

لإنشاء تأثير عكسي لـ dropShadow()، استخدِم Modifier.innerShadow()، ما يخلق الوهم بأنّ العنصر مجوّف أو مضغوط في السطح السفلي.

يكون الترتيب مهمًا عند إنشاء الظلال الداخلية. يتم رسم المُعدِّل innerShadow() أعلى المحتوى. للتأكد من أن الظل مرئي، فإنك عادةً ما تقوم بالخطوات التالية:

  1. ارسم محتوى الخلفية.
  2. طبِّق المُعدِّل innerShadow() لإنشاء المظهر المقعّر.

إذا تم وضع innerShadow() قبل الخلفية، يتم رسم الخلفية فوق الظل، ما يؤدي إلى إخفائه تمامًا.

يوضّح المثال التالي تطبيق innerShadow() على RoundedCornerShape:

@Composable
fun SimpleInnerShadowUsage() {
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .width(300.dp)
                .height(200.dp)
                .align(Alignment.Center)
                // note that the background needs to be defined before defining the inner shadow
                .background(
                    color = Color.White,
                    shape = RoundedCornerShape(20.dp)
                )
                .innerShadow(
                    shape = RoundedCornerShape(20.dp),
                    shadow = Shadow(
                        radius = 10.dp,
                        spread = 2.dp,
                        color = Color(0x40000000),
                        offset = DpOffset(x = 6.dp, 7.dp)
                    )
                )

        ) {
            Text(
                "Inner Shadow",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 32.sp
            )
        }
    }
}

ظل داخلي رمادي داخل شكل مستطيل أبيض
الشكل 3. مثال على تطبيق Modifier.innerShadow() على مستطيل بزوايا مستديرة

تحريك الظلال عند تفاعل المستخدم

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

ينشئ الرمز التالي تأثير "الضغط" مع ظل (وهم بأنّ السطح يتم دفعه لأسفل الشاشة):

@Composable
fun AnimatedColoredShadows() {
    SnippetsTheme {
        Box(Modifier.fillMaxSize()) {
            val interactionSource = remember { MutableInteractionSource() }
            val isPressed by interactionSource.collectIsPressedAsState()

            // Create transition with pressed state
            val transition = updateTransition(
                targetState = isPressed,
                label = "button_press_transition"
            )

            fun <T> buttonPressAnimation() = tween<T>(
                durationMillis = 400,
                easing = EaseInOut
            )

            // Animate all properties using the transition
            val shadowAlpha by transition.animateFloat(
                label = "shadow_alpha",
                transitionSpec = { buttonPressAnimation() }
            ) { pressed ->
                if (pressed) 0f else 1f
            }
            // ...

            val blueDropShadow by transition.animateColor(
                label = "shadow_color",
                transitionSpec = { buttonPressAnimation() }
            ) { pressed ->
                if (pressed) Color.Transparent else blueDropShadowColor
            }

            // ...

            Box(
                Modifier
                    .clickable(
                        interactionSource, indication = null
                    ) {
                        // ** ...... **//
                    }
                    .width(300.dp)
                    .height(200.dp)
                    .align(Alignment.Center)
                    .dropShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 10.dp,
                            spread = 0.dp,
                            color = blueDropShadow,
                            offset = DpOffset(x = 0.dp, -(2).dp),
                            alpha = shadowAlpha
                        )
                    )
                    .dropShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 10.dp,
                            spread = 0.dp,
                            color = darkBlueDropShadow,
                            offset = DpOffset(x = 2.dp, 6.dp),
                            alpha = shadowAlpha
                        )
                    )
                    // note that the background needs to be defined before defining the inner shadow
                    .background(
                        color = Color(0xFFFFFFFF),
                        shape = RoundedCornerShape(70.dp)
                    )
                    .innerShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 8.dp,
                            spread = 4.dp,
                            color = innerShadowColor2,
                            offset = DpOffset(x = 4.dp, 0.dp)
                        )
                    )
                    .innerShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 20.dp,
                            spread = 4.dp,
                            color = innerShadowColor1,
                            offset = DpOffset(x = 4.dp, 0.dp),
                            alpha = innerShadowAlpha
                        )
                    )

            ) {
                Text(
                    "Animated Shadows",
                    // ...
                )
            }
        }
    }
}

نقاط رئيسية حول الرمز

  • تحدّد هذه السمة حالات البدء والانتهاء للمَعلمات التي سيتم تحريكها عند الضغط باستخدام transition.animateColor وtransition.animateFloat.
  • يستخدم updateTransition ويوفّر له targetState (targetState = isPressed) المحدّد للتحقّق من مزامنة جميع الصور المتحركة. عندما يتغيّر isPressed، يدير عنصر الانتقال تلقائيًا الحركة لجميع الخصائص الفرعية من قيمها الحالية إلى القيم المستهدفة الجديدة.
  • تحدّد هذه السمة مواصفات buttonPressAnimation التي تتحكّم في توقيت الانتقال وتخفيفه. وتحدّد هذه السمة tween (اختصار لـ in-between) بمدة 400 ملي ثانية ومنحنى EaseInOut، ما يعني أنّ الحركة تبدأ ببطء، ثم تتسارع في المنتصف، ثم تتباطأ في النهاية.
  • تحدّد هذه السمة Box مع سلسلة من دوال التعديل التي تطبّق جميع الخصائص المتحركة لإنشاء العنصر المرئي، بما في ذلك ما يلي:
    • .clickable(): عنصر تعديل يجعل Box تفاعليًا.
    • ‫.dropShadow(): يتم تطبيق تأثيرَي تظليل قطرات خارجيَين أولاً. يتم ربط خصائص اللون والشفافية بالقيم المتحركة (blueDropShadow، وما إلى ذلك) وإنشاء المظهر المرتفع الأولي.
    • ‫.innerShadow(): يتم رسم ظلَّين داخليَّين فوق الخلفية. ترتبط خصائصها بالمجموعة الأخرى من القيم المتحركة (innerShadowColor1، وما إلى ذلك) وتنشئ المظهر المسنن.

النتيجة

الشكل 4. يظهر ظل متحرك عند ضغط المستخدم.

إنشاء ظلال متدرّجة

لا تقتصر الظلال على الألوان الثابتة. تقبل واجهة برمجة التطبيقات Shadow API مَعلمة Brush، ما يتيح لك إنشاء ظلال متدرّجة.

Box(
    modifier = Modifier
        .width(240.dp)
        .height(200.dp)
        .dropShadow(
            shape = RoundedCornerShape(70.dp),
            shadow = Shadow(
                radius = 10.dp,
                spread = animatedSpread.dp,
                brush = Brush.sweepGradient(
                    colors
                ),
                offset = DpOffset(x = 0.dp, y = 0.dp),
                alpha = animatedAlpha
            )
        )
        .clip(RoundedCornerShape(70.dp))
        .background(Color(0xEDFFFFFF)),
    contentAlignment = Alignment.Center
) {
    Text(
        text = breathingText,
        color = Color.Black,
        style = MaterialTheme.typography.bodyLarge
    )
}

نقاط رئيسية حول الرمز

  • تضيف السمة dropShadow() ظلاً خلف المربّع.
  • يُلوّن brush = Brush.sweepGradient(colors) الظل بتدرج يدور من خلال قائمة من colors المحددة مسبقًا، مما يخلق تأثيرًا يشبه قوس قزح.

النتيجة

يمكنك استخدام فرشاة كظل لإنشاء تدرج dropShadow() مع تأثير "تنفس":

الشكل 5. تظليل قطرات متدرّج متحرّك

دمج الظلال

يمكنك الجمع بين معدّلات dropShadow() وinnerShadow() ووضعها في طبقات لإنشاء مجموعة متنوعة من التأثيرات. توضّح لك الأقسام التالية كيفية إنشاء ظلال ناعمة وواقعية وبسيطة باستخدام هذه التقنية.

إنشاء ظلال نيومورفيك

تتميز الظلال النيومورفية بمظهر ناعم يظهر بشكل طبيعي من الخلفية. لإنشاء ظلال نيومورفيك، اتّبِع الخطوات التالية:

  1. استخدِم عنصرًا يتشارك الألوان نفسها مع خلفيته.
  2. استخدِم تظليلَين خفيفَين متعاكسَين: ظل فاتح في إحدى الزوايا، وظل داكن في الزاوية المقابلة.

يضع المقتطف التالي طبقتَين من معدِّل dropShadow() لإنشاء تأثير النيومورفيزم:

@Composable
fun NeumorphicRaisedButton(
    shape: RoundedCornerShape = RoundedCornerShape(30.dp)
) {
    val bgColor = Color(0xFFe0e0e0)
    val lightShadow = Color(0xFFFFFFFF)
    val darkShadow = Color(0xFFb1b1b1)
    val upperOffset = -10.dp
    val lowerOffset = 10.dp
    val radius = 15.dp
    val spread = 0.dp
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(bgColor)
            .wrapContentSize(Alignment.Center)
            .size(240.dp)
            .dropShadow(
                shape,
                shadow = Shadow(
                    radius = radius,
                    color = lightShadow,
                    spread = spread,
                    offset = DpOffset(upperOffset, upperOffset)
                ),
            )
            .dropShadow(
                shape,
                shadow = Shadow(
                    radius = radius,
                    color = darkShadow,
                    spread = spread,
                    offset = DpOffset(lowerOffset, lowerOffset)
                ),

            )
            .background(bgColor, shape)
    )
}

شكل مستطيل أبيض بتأثير ناعم على خلفية بيضاء
الشكل 6. تأثير ظل ناعم

إنشاء ظلال نيوبروتالية

يعرض أسلوب neobrutalist تخطيطات عالية التباين وممتلئة وألوانًا زاهية وحدودًا سميكة. لإنشاء هذا التأثير، استخدِم dropShadow() بدون تمويه وإزاحة مميّزة، كما هو موضّح في المقتطف التالي:

@Composable
fun NeoBrutalShadows() {
    SnippetsTheme {
        val dropShadowColor = Color(0xFF007AFF)
        val borderColor = Color(0xFFFF2D55)
        Box(Modifier.fillMaxSize()) {
            Box(
                Modifier
                    .width(300.dp)
                    .height(200.dp)
                    .align(Alignment.Center)
                    .dropShadow(
                        shape = RoundedCornerShape(0.dp),
                        shadow = Shadow(
                            radius = 0.dp,
                            spread = 0.dp,
                            color = dropShadowColor,
                            offset = DpOffset(x = 8.dp, 8.dp)
                        )
                    )
                    .border(
                        8.dp, borderColor
                    )
                    .background(
                        color = Color.White,
                        shape = RoundedCornerShape(0.dp)
                    )
            ) {
                Text(
                    "Neobrutal Shadows",
                    modifier = Modifier.align(Alignment.Center),
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

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

إنشاء ظلال واقعية

تحاكي الظلال الواقعية الظلال في العالم المادي - تظهر مضاءة بمصدر ضوء أساسي، مما ينتج عنه ظل مباشر وظل أكثر انتشارًا. يمكنك تجميع عدة نُسخ من dropShadow() وinnerShadow() بسمات مختلفة لإعادة إنشاء تأثيرات ظل واقعية، كما هو موضّح في المقتطف التالي:

@Composable
fun RealisticShadows() {
    Box(Modifier.fillMaxSize()) {
        val dropShadowColor1 = Color(0xB3000000)
        val dropShadowColor2 = Color(0x66000000)

        val innerShadowColor1 = Color(0xCC000000)
        val innerShadowColor2 = Color(0xFF050505)
        val innerShadowColor3 = Color(0x40FFFFFF)
        val innerShadowColor4 = Color(0x1A050505)
        Box(
            Modifier
                .width(300.dp)
                .height(200.dp)
                .align(Alignment.Center)
                .dropShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 40.dp,
                        spread = 0.dp,
                        color = dropShadowColor1,
                        offset = DpOffset(x = 2.dp, 8.dp)
                    )
                )
                .dropShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 4.dp,
                        spread = 0.dp,
                        color = dropShadowColor2,
                        offset = DpOffset(x = 0.dp, 4.dp)
                    )
                )
                // note that the background needs to be defined before defining the inner shadow
                .background(
                    color = Color.Black,
                    shape = RoundedCornerShape(100.dp)
                )
// //
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 12.dp,
                        spread = 3.dp,
                        color = innerShadowColor1,
                        offset = DpOffset(x = 6.dp, 6.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 4.dp,
                        spread = 1.dp,
                        color = Color.White,
                        offset = DpOffset(x = 5.dp, 5.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 12.dp,
                        spread = 5.dp,
                        color = innerShadowColor2,
                        offset = DpOffset(x = (-3).dp, (-12).dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 3.dp,
                        spread = 10.dp,
                        color = innerShadowColor3,
                        offset = DpOffset(x = 0.dp, 0.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 3.dp,
                        spread = 9.dp,
                        color = innerShadowColor4,
                        offset = DpOffset(x = 1.dp, 1.dp)
                    )
                )

        ) {
            Text(
                "Realistic Shadows",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 24.sp,
                color = Color.White
            )
        }
    }
}

نقاط رئيسية حول الرمز

  • يتم تطبيق مفتاحَي تعديل dropShadow() متسلسلَين بسمات مختلفة، يليهما مفتاح تعديل background().
  • يتم تطبيق معدِّلات innerShadow() المتسلسلة لإنشاء تأثير الحافة المعدنية حول حافة المكوّن.

النتيجة

ينتج عن مقتطف الرمز السابق ما يلي:

ظل أبيض واقعي حول شكل دائري أسود
الشكل 8 تأثير ظل واقعي