В этой статье я расскажу, как создать модификатор 3D-границы (3D Border Modifier) для Jetpack Compose, применимый к любому представлению с любой формой. Кроме того, вы узнаете, как создать панель поиска с помощью этого модификатора.

Выпуклая граница (Convex Border)

Начнем с определения основной функции расширения convexBorder для Modifier, которая в конечном итоге нарисует выпуклую границу.

ConvexStyle

Сначала для наглядности создадим класс данных ConvexStyle для представления стиля с эффектом выпуклости, применяемым к границе.

data class ConvexStyle(
val blur: Dp = 3.dp,
val offset: Dp = 2.dp,
val glareColor: Color = Color.White.copy(0.64f),
val shadowColor: Color = Color.Black.copy(0.64f)
)

Функция

Теперь все готово для определения функции:

fun Modifier.convexBorder(
color: Color,
shape: Shape,
strokeWidth: Dp = 8.dp,
convexStyle: ConvexStyle = ConvexStyle()
)
  • color ➜ цвет границы.
  • shape ➜ форма границы.
  • strokeWidth ➜ ширина обводки границы.
  • convexStyle ➜ стиль с эффектом выпуклости, применяемым к границе.

Реализация

Теперь можно перейти к реализации.

Рисование тени и бликов

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

fun DrawScope.drawConvexBorderShadow(
outline: Outline,
strokeWidth: Dp,
blur: Dp,
offsetX: Dp,
offsetY: Dp,
shadowColor: Color
) = drawIntoCanvas { canvas ->
// Создайте и настройте объект Paint
val shadowPaint = Paint().apply {
this.style = PaintingStyle.Stroke
this.color = shadowColor
this.strokeWidth = strokeWidth.toPx()
}

// Сохраните текущий слой перед трансформацией
canvas.saveLayer(size.toRect(), shadowPaint)

val halfStrokeWidth = strokeWidth.toPx() / 2
// Переместите canvas так, чтобы граница поместилась в рамки
canvas.translate(halfStrokeWidth, halfStrokeWidth)
// Нарисуйте контур тени
canvas.drawOutline(outline, shadowPaint)

// Примените режим наложения и эффект размытия для тени
shadowPaint.asFrameworkPaint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
maskFilter = BlurMaskFilter(blur.toPx(), BlurMaskFilter.Blur.NORMAL)
}
// Задайте цвет для обрезки
shadowPaint.color = Color.Black

// Переместите canvas и нарисуйте контур обрезания тени
canvas.translate(offsetX.toPx(), offsetY.toPx())
canvas.drawOutline(outline, shadowPaint)
// Верните canvas в исходное состояние
canvas.restore()
}

Чтобы лучше понять, как работает эта функция, посмотрите на рисунок ниже.

Реализация convexBorder

Теперь, вооружившись функцией drawConvexBorderShadow, можно определить основную функцию для рисования выпуклой границы.

fun Modifier.convexBorder(
/* Параметры... */
) = this.drawWithContent {
// Настройте размер, чтобы он соответствовал границам canvas
val adjustedSize = Size(size.width - strokeWidth.toPx(), size.height - strokeWidth.toPx())
// Создайте контур на основе формы и скорректированного размера
val outline = shape.createOutline(adjustedSize, layoutDirection, this)

// Нарисуйте оригинальное содержимое композиции
drawContent()

// Переместите canvas так, чтобы граница поместилась в рамки
translate(halfStrokeWidth, halfStrokeWidth) {
// Нарисуйте контур основной границы
drawOutline(
outline = outline,
color = color,
style = Stroke(width = strokeWidth.toPx())
)
}

with(convexStyle) {
// Нарисуйте контур тени
drawConvexBorderShadow(outline, strokeWidth, blur, -offset, -offset, shadowColor)
// Нарисуйте контур блика
drawConvexBorderShadow(outline, strokeWidth, blur, offset, offset, glareColor)
}
}

Вот как это работает:

Мы успешно создали границу. Полный код реализации размещен на GitHub Gist. Теперь посмотрим, как создать пользовательскую панель поиска с помощью этой функции.

Практический пример 

Перейдем к практической части статьи.

Чтобы написать TextField с индивидуальным стилем, будем использовать параметр decorationBox в BasicTextField.

// Переменное состояние для хранения вводимого текста
var text by remember { mutableStateOf("") }

BasicTextField(
value = text,
onValueChange = { text = it },
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences,
imeAction = ImeAction.Search
),
textStyle = LocalTextStyle.current.copy(
fontSize = 16.sp,
fontWeight = FontWeight.Medium
),
decorationBox = { innerTextField ->
Row(
modifier = Modifier
.size(350.dp, 60.dp)
// Установите цвет и форму фона
.background(Color(0xFF7F2DBF), CircleShape)
// Примените выпуклую границу того же цвета и формы.
.convexBorder(Color(0xFF7F2DBF), CircleShape)
.padding(horizontal = 20.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Добавьте иконку поиска
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null
)
Box {
// Показывать текст-заполнитель, когда вводимый текст пуст
if (text.isEmpty()) {
Text(
text = "Search...",
style = LocalTextStyle.current.copy(color = Color(0xFF242424))
)
}
// Отображение фактического текстового поля
innerTextField()
}
}
}
)

Результат

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Kappdev: How to Create a Stunning 3D Border in Jetpack Compose

Предыдущая статьяИзучаем gRPC и Flutter для разработки современных приложений
Следующая статьяОптимизация начальной загрузки сервера с RocksDB