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

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

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

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

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

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

تنشئ Modifier.shadow() ظلًا أساسيًا يتّبع إرشادات Material Design التي تحاكي مصدر ضوء من الأعلى. يستند عمق الظل إلى قيمة 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. ظلّ يتحرّك عند ضغط المستخدم على الشاشة

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

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

إنشاء ظلال بأسلوب "الوحشية الجديدة"

يتميّز أسلوب التصميم النيوبروتاليست بتنسيقات ذات تباين عالٍ ومربّعات وألوان زاهية وحدود سميكة. لإنشاء هذا التأثير، استخدِم 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. تأثير ظل واقعي