Connect with us

Разработка

Как создать кастомный угловой бейдж в Jetpack Compose

В этой статье мы рассмотрим, как создать настраиваемый угловой бейдж в Jetpack Compose. Этот значок может быть особенно полезен для отображения скидок, ярлыков новых товаров или любых других отметок.

Опубликовано

/

     
     

В этой статье мы рассмотрим, как создать настраиваемый угловой бейдж в Jetpack Compose. Этот значок может быть особенно полезен для отображения скидок, ярлыков новых товаров или любых других отметок. Мы также позаботимся о том, чтобы содержимое значка было полностью настраиваемым.

Давайте погрузимся в работу!

Идея

Идея заключается в том, чтобы создать значок в виде полоски, который будет располагаться в углу компонента, повернутый на 45°, с настраиваемыми свойствами. Чтобы добавить глубины, мы нарисуем две дополнительные полоски позади.

Вот схематичная иллюстрация:

Как создать кастомный угловой бейдж в Jetpack Compose

Создание кастомной формы

Сначала определим кастомный Shape под названием CornerBadgeShape, который позволит нам создать полосу с регулируемой округлостью:

class CornerBadgeShape(
    @FloatRange(0.0, 1.0)
    private val cornerRoundness: Float,
) : Shape {

    override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
        val (width, height) = size
        val cornerLength = computeAdjustedCornerLength(width, height, cornerRoundness)

        val path = Path().apply {
            moveTo(0f, height)

            // 1
            lineTo(x = height - cornerLength / 2, y = cornerLength / 2)

            // 2
            cubicTo(
                x1 = height, y1 = 0f,
                x2 = height + cornerLength / 3, y2 = 0f,
                x3 = height + cornerLength, y3 = 0f
            )

            // 3
            lineTo(x = width - height - cornerLength, y = 0f)

            // 4
            cubicTo(
                x1 = width - height - cornerLength / 3, y1 = 0f,
                x2 = width - height, y2 = 0f,
                x3 = width - height + cornerLength / 2, y3 = cornerLength / 2
            )

            // 5
            lineTo(x = width, height)

            // 6
            cubicTo(
                x1 = width - cornerLength / 2, y1 = height - cornerLength / 2,
                x2 = width - height * 0.66f, y2 = height - cornerLength / 2,
                x3 = width - height, y3 = height - cornerLength / 2
            )

            // 7
            lineTo(x = height, y = height - cornerLength / 2)

            // 8
            cubicTo(
                x1 = height * 0.66f, y1 = height - cornerLength / 2,
                x2 = cornerLength / 2, y2 = height - cornerLength / 2,
                x3 = 0f, y3 = height
            )

            close()
        }

        return Outline.Generic(path)
    }

    companion object {

        private const val BASE_CORNER_RATIO = 1f / 3f

        fun computeAdjustedCornerLength(width: Float, height: Float, cornerRoundness: Float): Float {
            val targetCornerLength = height * BASE_CORNER_RATIO * cornerRoundness.coerceIn(0f, 1f)
            val maxCornerLength = computeMaxCornerFitLength(width, height).coerceAtLeast(0f)
            return targetCornerLength.coerceAtMost(maxCornerLength)
        }

        private fun computeMaxCornerFitLength(width: Float, height: Float): Float {
            return (width - height * 2) / 2
        }
    }
}

Схема рисования фигуры:

Как создать кастомный угловой бейдж в Jetpack Compose

Углы бейджа

Чтобы расположить бейдж в любом из четырех углов, мы определим sealed класс BadgeCorner, который представляет каждый вариант угла вместе с информацией о его выравнивании и зеркальном отображении:

sealed class BadgeCorner(
    val alignment: Alignment,
    val layoutScaleX: Float,
    val layoutScaleY: Float,
    val innerScaleX: Float,
    val innerScaleY: Float
) {
    object TopLeft: BadgeCorner(Alignment.TopStart, 1f, 1f, 1f, 1f)
    object TopRight: BadgeCorner(Alignment.TopEnd, -1f, 1f, -1f, 1f)
    object BottomLeft: BadgeCorner(Alignment.BottomStart, 1f, -1f, 1f, -1f)
    object BottomRight: BadgeCorner(Alignment.BottomEnd, -1f, -1f, -1f, -1f)
}

Мы используем scaleX = -1 для горизонтального зеркального отображения и scaleY = -1 для вертикального зеркального отображения, чтобы соответствующим образом перевернуть бейдж в зависимости от угла. Чтобы сохранить правильную ориентацию содержимого внутри бейджа, мы соответствующим образом изменяем innerScale, чтобы противодействовать этому зеркальному отражению.

Создание композабл

Теперь давайте определим составной CornerBadgeBox, который объединяет все вместе:

@Composable
fun CornerBadgeBox(
    badgeBrush: Brush,
    backBadgeBrush: Brush,
    modifier: Modifier = Modifier,
    corner: BadgeCorner = BadgeCorner.TopRight,
    contentPadding: Dp = 4.dp,
    stripThickness: Dp = 32.dp,
    cornerPadding: Dp = 32.dp,
    cornerRoundness: Float = 0.5f,
    badgeContent: @Composable BoxScope.() -> Unit,
    content: @Composable BoxScope.() -> Unit
) {
    // Implementation goes here
}

Параметры:

  • badgeBrush — кисть, используемая для рисования видимой полосы бейджа;
  • backBadgeBrush — кисть, используемая для декоративных задних полос;
  • modifier — модификатор для стилизации внешнего контейнера;
  • corner — указывает угол, в котором отображается бейдж;
  • contentPadding — расстояние между краями содержимого и бейджем;
  • stripThickness — толщина полос бейджа. Видимая толщина может казаться меньше из-за примененного закругления углов;
  • cornerPadding — расстояние от угла до начала бейджа;
  • cornerRoundness — округлость углов бейджа (от 0f до 1f);
  • badgeContent — Composable содержимое внутри бейджа;
  • content — основное содержимое контейнера.

Как создать кастомный угловой бейдж в Jetpack Compose

Реализация макета

Прежде чем приступить к созданию макета, давайте определим служебную функцию для вычисления диагонали квадрата. Она поможет нам определить размер бейджа и рассчитать нужную подложку:

private fun squareDiagonal(side: Dp): Dp {
    return squareDiagonal(side.value).dp
}

private fun squareDiagonal(side: Float): Float {
    return side * sqrt(2f)
}

Теперь мы можем приступить к реализации макета внутри композита CornerBadgeBox.

Это включает в себя соответствующее позиционирование задних линий бейджа, основного контента и главного бейджа на переднем плане с помощью композабл Layout и применение необходимых преобразований.

Посмотрите сниппет:

// Define the badge shape
val badgeShape = remember(cornerRoundness) { CornerBadgeShape(cornerRoundness) }

// Pre-calculate a padding that occurs because of the roundness of the strip
val stripRoundnessPadding = remember(stripThickness, cornerRoundness, contentPadding, cornerPadding) {
    val badgeSideLength = squareDiagonal(stripThickness)
    val badgeWidth = squareDiagonal(contentPadding + cornerPadding + badgeSideLength)
    CornerBadgeShape.computeAdjustedCornerLength(badgeWidth.value, stripThickness.value, cornerRoundness).dp / 2
}

Layout(
    modifier = modifier,
    content = {
        // Side back badge: rotated 45° and mirrored vertically
        Box(
            Modifier
                .layoutId(LayoutId.SideBackBadge)
                .scale(corner.layoutScaleX, corner.layoutScaleY)
                .graphicsLayer {
                    transformOrigin = TransformOrigin(0f, 1f)
                    rotationZ = 45f
                    scaleY = -1f
                }
                .background(backBadgeBrush, badgeShape)
        )

        // Top back badge: rotated 45° without mirroring
        Box(
            Modifier
                .layoutId(LayoutId.TopBackBadge)
                .scale(corner.layoutScaleX, corner.layoutScaleY)
                .graphicsLayer {
                    transformOrigin = TransformOrigin(0f, 1f)
                    rotationZ = 45f
                }
                .background(backBadgeBrush, badgeShape)
        )

        // Main content of the box
        Box(
            modifier = Modifier.layoutId(LayoutId.Content),
            content = content
        )

        // Foreground badge on top of the content
        Box(
            content = badgeContent,
            contentAlignment = Alignment.Center,
            modifier = Modifier
                .layoutId(LayoutId.Badge)
                .scale(corner.layoutScaleX, corner.layoutScaleY)
                .graphicsLayer {
                    transformOrigin = TransformOrigin(0f, 1f)
                    rotationZ = -45f
                }
                .background(badgeBrush, badgeShape)
                .clip(badgeShape)
                // Add bottom padding to account for corner roundness
                .padding(bottom = stripRoundnessPadding)
                .scale(corner.innerScaleX, corner.innerScaleY)
        )
    }
) { measurables, constraints ->
    // Convert dimensions from Dp to pixels
    val contentPaddingPx = contentPadding.roundToPx()
    val stripThicknessPx = stripThickness.roundToPx()
    val cornerPaddingPx = cornerPadding.roundToPx()

    // Calculate the side length of the strip when rotated 45°
    val badgeSideLength = squareDiagonal(stripThicknessPx.toFloat())
    // Calculate the diagonal width of the badge strip’s bounding square
    val badgeWidth = squareDiagonal(contentPaddingPx + cornerPaddingPx + badgeSideLength)

    // Calculate the padding that should be applied to the back badges from the corner
    val stripRoundnessPadding = CornerBadgeShape.computeAdjustedCornerLength(badgeWidth, stripThicknessPx.toFloat(), cornerRoundness) / 2
    val backBadgeCornerPadding = contentPaddingPx + cornerPaddingPx + squareDiagonal(stripRoundnessPadding)

    // Measure all components
    val contentPlaceable = measurables.first { it.layoutId == LayoutId.Content }.measure(constraints)
    val badgePlaceable = measurables.first { it.layoutId == LayoutId.Badge }.measure(
        Constraints.fixed(badgeWidth.toInt(), stripThicknessPx)
    )
    val sideBackBadgePlaceable = measurables.first { it.layoutId == LayoutId.SideBackBadge }.measure(
        Constraints.fixed(badgeWidth.toInt(), stripThicknessPx)
    )
    val topBackBadgePlaceable = measurables.first { it.layoutId == LayoutId.TopBackBadge }.measure(
        Constraints.fixed(badgeWidth.toInt(), stripThicknessPx)
    )

    val layoutWidth = contentPlaceable.width
    val layoutHeight = contentPlaceable.height

    layout(layoutWidth, layoutHeight) {
        // Compute the badge position based on corner alignment
        val badgePosition = corner.alignment.align(
            size = IntSize(badgePlaceable.width, badgePlaceable.height),
            space = IntSize(layoutWidth + contentPaddingPx * 2, layoutHeight + contentPaddingPx * 2),
            layoutDirection = LayoutDirection.Ltr
        ) - IntOffset(contentPaddingPx, contentPaddingPx)

        // Place the side back badge
        sideBackBadgePlaceable.placeWithLayer(badgePosition) {
            translationY = (-stripThicknessPx + backBadgeCornerPadding) * corner.layoutScaleY
        }

        // Place the top back badge
        topBackBadgePlaceable.placeWithLayer(badgePosition) {
            translationY = -stripThicknessPx * corner.layoutScaleY
            translationX = backBadgeCornerPadding * corner.layoutScaleX
        }

        // Place the main content
        contentPlaceable.placeRelative(0, 0)

        // Place the foreground badge
        badgePlaceable.placeWithLayer(badgePosition) {
            val offset = contentPaddingPx + cornerPaddingPx + badgeSideLength
            translationY = (-stripThicknessPx + offset) * corner.layoutScaleY
        }
    }
}

Мы закончили! Если вам нужен полный код, то посмотрите его на на GitHub.

Практическое использование

Давайте рассмотрим несколько практических примеров.

Простой значок скидки

Самый простой вариант использования — это отображение короткой текстовой надписи — например, скидки в данном случае:

CornerBadgeBox(
    badgeBrush = SolidColor(Color(0xFFFFB907)),
    backBadgeBrush = SolidColor(Color(0xFFD9951C)),
    stripThickness = 32.dp,
    cornerPadding = 32.dp,
    contentPadding = 4.dp,
    cornerRoundness = 0.5f,
    corner = BadgeCorner.TopRight,
    badgeContent = {
        Text(
            text = "34% OFF",
            fontSize = 12.sp,
            fontWeight = FontWeight.Medium,
        )
    }
) {
    Column(
        Modifier
            .shadow(6.dp, RoundedCornerShape(12.dp))
            .background(MaterialTheme.colorScheme.surfaceContainerHigh)
    ) {
        Image(
            painter = painterResource(R.drawable.apple),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.size(200.dp, 230.dp)
        )
        Text(
            text = "Apple",
            fontSize = 16.sp,
            modifier = Modifier.padding(8.dp)
        )
    }
}

Как создать кастомный угловой бейдж в Jetpack Compose

Анимированный бейдж распродажи

Мы также можем создать анимированный значок «SALE», расширив предыдущий пример и применив простой модификатор basicMarquee:

CornerBadgeBox(
    // Same parameters as before
    corner = BadgeCorner.TopLeft,
    badgeContent = {
        Text(
            text = "SALE • SALE • SALE • SALE • SALE • SALE • ",
            fontSize = 12.sp,
            maxLines = 1,
            modifier = Modifier.basicMarquee(
                iterations = Int.MAX_VALUE,
                repeatDelayMillis = 0,
                spacing = MarqueeSpacing.fractionOfContainer(0f),
                velocity = 12.dp
            )
        )
    }
) {
    // Same content as before
}

Бейдж избранного

Еще один вариант использования — создание отметки для отмеченных элементов:

CornerBadgeBox(
    badgeBrush = SolidColor(Color(0xFFF89D44)),
    backBadgeBrush = SolidColor(Color(0xFFBD6919)),
    corner = BadgeCorner.TopRight,
    stripThickness = 42.dp,
    cornerPadding = 4.dp,
    contentPadding = 2.dp,
    cornerRoundness = 0.5f,
    badgeContent = {
        Icon(
            imageVector = Icons.Rounded.Star,
            contentDescription = null,
            tint = Color(0xFFFD5F0B),
            modifier = Modifier
                .size(28.dp)
                .align(Alignment.Center)
                .rotate(-45f)
        )
    }
) {
    Column(
        Modifier
            .shadow(6.dp, RoundedCornerShape(12.dp))
            .background(MaterialTheme.colorScheme.surfaceContainerHigh)
    ) {
        Image(
            painter = painterResource(R.drawable.banana),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.size(200.dp, 230.dp)
        )
        Text(
            text = "Banana",
            fontSize = 16.sp,
            modifier = Modifier.padding(8.dp)
        )
    }
}

Как создать кастомный угловой бейдж в Jetpack Compose

Возможно, вы придумаете еще более креативные способы его использования — получайте удовольствие.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: