افزودن سایه در «نوشتن»

سایه‌ها به‌صورت دیداری میانای کاربر را ارتقا می‌دهند، تعامل‌پذیری را به کاربران نشان می‌دهند، و بازخورد فوری درباره کنش‌های کاربر ارائه می‌دهند. Compose چندین روش برای گنجاندن سایه در برنامه ارائه می‌دهد:

  • Modifier.shadow(): سایه‌ای مبتنی بر ارتفاع در پشت یک composable ایجاد می‌کند که با دستورالعمل‌های Material Design مطابقت دارد.
  • Modifier.dropShadow(): سایه‌ای قابل‌سفارشی‌سازی ایجاد می‌کند که پشت یک composable ظاهر می‌شود و آن را برجسته نشان می‌دهد.
  • Modifier.innerShadow(): سایه‌ای در داخل مرزهای یک composable ایجاد می‌کند و باعث می‌شود که به‌نظر برسد به سطح پشت آن فشار داده شده است.

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

پویانمایی سایه‌ها در تعامل کاربر

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

نتیجه

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

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

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

شکل ۵. سایه گرادیان پویانمایی‌شده.

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

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

نکات کلیدی درباره کد

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

نتیجه

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

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