سایه ها را در 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)
        )
    }
}

سایه‌ای خاکستری دور یک شکل مستطیلی سفید افتاده است.
شکل ۱. سایه‌ای مبتنی بر ارتفاع که با استفاده از Modifier.shadow() ایجاد شده است.

سایه‌های برجسته را پیاده‌سازی کنید

از اصلاحگر dropShadow() برای رسم سایه‌ای دقیق پشت محتوای خود استفاده کنید، که باعث می‌شود عنصر برجسته‌تر به نظر برسد.

شما می‌توانید جنبه‌های کلیدی زیر را از طریق پارامتر Shadow کنترل کنید:

  • radius : میزان نرمی و پراکندگی محوشدگی (blur) شما را تعریف می‌کند.
  • 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 با رنگ سفید پر شده است.
    • پس‌زمینه به همان شکل مستطیل گوشه گرد سایه، برش داده می‌شود.

نتیجه

یک سایه خاکستری که دور یک شکل مستطیلی سفید افتاده است.
شکل ۲. سایه‌ای که دور شکل کشیده شده است.

سایه‌های داخلی را پیاده‌سازی کنید

برای ایجاد یک اثر معکوس برای 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
            )
        }
    }
}

یک سایه داخلی خاکستری درون یک شکل مستطیلی سفید.
شکل ۳. کاربردی از 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 تغییر کند، شیء transition به طور خودکار انیمیشن همه ویژگی‌های فرزند را از مقادیر فعلی آنها به مقادیر هدف جدید مدیریت می‌کند.
  • مشخصات buttonPressAnimation را تعریف می‌کند که زمان‌بندی و کاهش سرعت انتقال را کنترل می‌کند. این ویژگی یک tween (مخفف in-between) با مدت زمان ۴۰۰ میلی‌ثانیه و یک منحنی EaseInOut مشخص می‌کند، به این معنی که انیمیشن با سرعت کم شروع می‌شود، در میانه سرعت می‌گیرد و در پایان سرعتش کم می‌شود.
  • یک Box با زنجیره‌ای از توابع اصلاح‌کننده تعریف می‌کند که تمام ویژگی‌های متحرک را برای ایجاد عنصر بصری اعمال می‌کنند، از جمله موارد زیر:
    • clickable() : یک اصلاح‌کننده که باعث می‌شود Box تعاملی باشد.
    • .dropShadow() : ابتدا دو سایه بیرونی اعمال می‌شوند. ویژگی‌های رنگ و آلفای آنها به مقادیر متحرک ( blueDropShadow و غیره) پیوند داده می‌شوند و ظاهر برجسته اولیه را ایجاد می‌کنند.
    • .innerShadow() ‎: دو سایه داخلی روی پس‌زمینه رسم می‌شوند. ویژگی‌های آنها به مجموعه دیگری از مقادیر متحرک ( innerShadowColor1 و غیره) مرتبط هستند و ظاهر تورفتگی ایجاد می‌کنند.

نتیجه

شکل ۴. سایه‌ای که با فشار کاربر متحرک می‌شود.

ایجاد سایه‌های گرادیان

سایه‌ها محدود به رنگ‌های یکدست نیستند. 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() با انیمیشن "تنفس" استفاده کنید:

شکل ۵. یک سایه گرادیان متحرک.

سایه‌ها را با هم ترکیب کنید

شما می‌توانید اصلاح‌کننده‌های 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)
    )
}

یک شکل مستطیلی سفید با جلوه نئومورفیک در برابر پس‌زمینه سفید.
شکل ۶. یک اثر سایه نئومورفیک.

سایه‌های نئوبروتالیستی ایجاد کنید

سبک نئوبروتالیستی، طرح‌بندی‌های بلوکی با کنتراست بالا، رنگ‌های زنده و حاشیه‌های ضخیم را به نمایش می‌گذارد. برای ایجاد این جلوه، از تابع 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
                )
            }
        }
    }
}

یک حاشیه قرمز دور یک مستطیل سفید با سایه آبی در برابر پس‌زمینه زرد.
شکل ۷. یک اثر سایه‌ای نئوبروتالیستی.

ایجاد سایه‌های واقع‌گرایانه

سایه‌های واقع‌گرایانه، سایه‌های دنیای فیزیکی را تقلید می‌کنند - به نظر می‌رسد که توسط یک منبع نور اصلی روشن می‌شوند، که منجر به ایجاد سایه مستقیم و سایه پراکنده‌تر می‌شود. می‌توانید چندین نمونه 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
            )
        }
    }
}

نکات کلیدی در مورد کد

  • دو اصلاح‌کننده‌ی (modifier dropShadow() زنجیره‌ای با ویژگی‌های متمایز اعمال می‌شوند و پس از آنها یک اصلاح‌کننده‌ی (modifier background() قرار می‌گیرد.
  • اصلاح‌کننده‌های innerShadow() به صورت زنجیره‌ای برای ایجاد جلوه‌ی حاشیه‌ی فلزی در اطراف لبه‌ی قطعه اعمال می‌شوند.

نتیجه

قطعه کد قبلی نتیجه زیر را تولید می‌کند:

یک سایه واقع‌گرایانه سفید در اطراف یک شکل گرد سیاه.
شکل ۸. یک افکت سایه واقع‌گرایانه.