Как добавить тени в Compose

Тени визуально приподнимают элементы интерфейса, указывают на их интерактивность и обеспечивают немедленную обратную связь при действиях пользователя. Compose предоставляет несколько способов включения теней в ваше приложение:

  • Modifier.shadow() создает тень за composable-функцией, которая соответствует рекомендациям Material Design.
  • Modifier.dropShadow() создает настраиваемую тень за composable-функцией, чтобы она выглядела приподнятой.
  • Modifier.innerShadow() создает тень внутри границ composable-функции, чтобы она выглядела вдавленной в поверхность за ней.

Modifier.shadow() подходит для создания простых теней, а модификаторы dropShadow() и innerShadow() позволяют точнее настраивать отрисовку теней.

На этой странице описывается, как реализовать каждый из этих модификаторов, в том числе как анимировать тени при взаимодействии с пользователем и как связать модификаторы innerShadow() и dropShadow() для создания градиентных теней, неоморфических теней и многого другого.

Создать простые тени

Modifier.shadow() создает базовую тень в соответствии с рекомендациями Material Design, имитируя верхний источник света. Глубина тени зависит от значения elevation, а отбрасываемая тень обрезается по форме composable-функции.

@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() в цепочку composable-функций, указав радиус, цвет и распространение. Обратите внимание, что фон 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() к прямоугольнику со скругленными углами.

Анимировать тени при взаимодействии с пользователем

Чтобы тени реагировали на действия пользователя, можно интегрировать их свойства с Animation 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. Реалистичный эффект тени.