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

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

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

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

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

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

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: تحدّد موضع هندسة الظل على المحورَين x وy.
  • 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. ظل يتحرّك عند ضغط المستخدم

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

لا تقتصر الظلال على الألوان الثابتة. تقبل واجهة برمجة التطبيقات للظل 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() وتراكبهما لإنشاء مجموعة متنوعة من التأثيرات. توضّح لك الأقسام التالية كيفية إنشاء ظلال neumorphic وneobrutalist وواقعية باستخدام هذه التقنية.

إنشاء ظلال neumorphic

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

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

يركّب المقتطف التالي معدّلين dropShadow() لإنشاء التأثير neumorphic:

@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. تأثير ظل neumorphic

إنشاء ظلال neobrutalist

يعرض نمط 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. تأثير ظل neobrutalist

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

تحاكي الظلال الواقعية الظلال في العالم المادي، وتبدو مضاءة بمصدر ضوء أساسي، ما يؤدي إلى ظهور ظل مباشر وظل أكثر انتشارًا. يمكنك تجميع عدة مثيلات من 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. تأثير ظل واقعي