הוספת צללים במצב כתיבה

הצללות משפרות את המראה של ממשק המשתמש, מציינות למשתמשים שיש אינטראקטיביות ומספקות משוב מיידי על פעולות המשתמשים. ב-Compose יש כמה דרכים לשלב צלליות באפליקציה:

  • Modifier.shadow(): יוצר צל שמבוסס על גובה מאחורי רכיב שאפשר להרכיב שמתאים להנחיות של Material Design.
  • 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: ממקם את הגיאומטריה של הצל לאורך הצירים x ו-y.
  • spread: קובע את ההתרחבות או ההתכווצות של הצורה של הצל.

בנוסף, הפרמטר shape מגדיר את הצורה הכוללת של הצל. אפשר להשתמש בכל צורה גיאומטרית מחבילת androidx.compose.foundation.shape, וגם בצורות המיוחדות של Material.

כדי להטמיע הטלת צללית בסיסית, מוסיפים את המשנה 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 מלא בצבע לבן.
    • הרכיב background נחתך לצורה של מלבן מעוגל כמו הצל.

התוצאה

צללית אפורה מסביב לצורה מלבנית לבנה.
איור 2. צללית ששורטטה מסביב לצורה.

הטמעה של צללים פנימיים

כדי ליצור אפקט הפוך ל-dropShadow, משתמשים ב-Modifier.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() במלבן עם פינות מעוגלות.

הנפשת הצללים באינטראקציה עם המשתמש

כדי שהצללים יגיבו לאינטראקציות של המשתמשים, אפשר לשלב מאפייני צל עם ממשקי ה-API של 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. אפקט צל ניאומורפי.

יצירת צללים בסגנון ניאו-ברוטליסטי

הסגנון הניאו-ברוטליסטי מתאפיין בניגודיות גבוהה, בפריסות מגושמות, בצבעים עזים ובגבולות עבים. כדי ליצור את האפקט הזה, משתמשים ב-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. אפקט צל שנראה מציאותי.