В этой статье я расскажу, как создать модификатор 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()
}
}
}
)
Результат
Читайте также:
- Создание кастомизированного кругового загрузчика в Jetpack Compose: изучение Android Canvas и анимации
- Реализация подсказок с помощью Modifier в Jetpack Compose
- Как сделать анимированную кнопку загрузки с Jetpack Compose
Читайте нас в Telegram, VK и Дзен
Перевод статьи Kappdev: How to Create a Stunning 3D Border in Jetpack Compose