Добавить тени в Compose

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

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

Чтобы тени реагировали на действия пользователя, вы можете интегрировать свойства теней с 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 ) длительностью 400 миллисекунд и кривую EaseInOut , что означает, что анимация начинается медленно, ускоряется в середине и замедляется в конце.
  • Определяет Box с цепочкой функций-модификаторов, которые применяют все анимированные свойства для создания визуального элемента, включая следующие:
    • clickable() : Модификатор, делающий Box интерактивным.
    • .dropShadow() : Сначала применяются две внешние тени. Их цвет и альфа-канал связаны с анимированными значениями ( blueDropShadow и т. д.) и создают первоначальный эффект приподнятости.
    • .innerShadow() : На фоне рисуются две внутренние тени. Их свойства связаны с другим набором анимированных значений ( innerShadowColor1 и т. д.) и создают эффект отступа.

Результат

Рисунок 4. Анимированная тень при нажатии пользователем.

Создание градиентных теней

Тени не ограничиваются сплошными цветами. 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. Реалистичный эффект тени.