У цьому туторіалі ми створимо анімований логотип Perplexity, що має ефект матового скла. Ми використаємо бібліотеку haze від Chris Banes для імітації скла та graphicsLayer для 3D-анімації

Налаштування проєкту
Спочатку необхідно додати залежність для haze у build.gradle:
repositories {
mavenCentral()
}
dependencies {
implementation("dev.chrisbanes.haze:haze:<version>")
}
Основний компонент
PerplexityLogo містить два блоки (RoundedBoxLeft і RoundedBoxRight), які створюють ефект матового скла.
💡 Таке рішення було зроблено через те, що після обертання елемента на 180° зображення відзеркалювалося і не давало такий ефект, як я хотів. На зображенні знизу видно, що жовтий є як зліва, так і зправа

@Composable
fun PerplexityLogo(
hazeStateLeft: HazeState,
hazeStateRight: HazeState,
modifier: Modifier
) {
val containerColor = MaterialTheme.colorScheme.surface
val hazeStyle = HazeStyle(
backgroundColor = containerColor,
tints = listOf(
HazeTint(containerColor.copy(alpha = if (containerColor.luminance() >= 0.5) 0.3f else 0.1f))
),
blurRadius = 10.dp,
noiseFactor = 0.3f
)
val infiniteTransition = rememberInfiniteTransition()
val animationProgress by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(5_000),
repeatMode = RepeatMode.Restart
)
)
Box(modifier = modifier) {
val pages = 8
MutableList(pages) { index ->
val rotationAngle = (animationProgress * 360 + (index * 360 / pages)) % 360
if (rotationAngle in 0f..180f) {
RoundedBoxLeft(rotationAngle - 90f, hazeStateLeft, hazeStyle, Modifier)
} else {
RoundedBoxRight(rotationAngle + 90f, hazeStateRight, hazeStyle, Modifier)
}
}
}
}
1/ Я створив свій HazeStyle, бо стандартні не дуже підходили, тай хотілося погратися з різним blurRadius та noiseFactor
val hazeStyle = HazeStyle(
backgroundColor = containerColor,
tints = listOf(
HazeTint(containerColor.copy(alpha = if (containerColor.luminance() >= 0.5) 0.3f else 0.1f))
),
blurRadius = 10.dp,
noiseFactor = 0.3f
)
2/ Створюємо 8 сторінок та вираховуємо для кожної з них кут. Залежно від кута вони будуть або злівої чи правої сторони. Ми віднімаємо 90 чи додаємо 90 градусів, щоб у нас сторінки малювалися з однієї точки, залежно від того, звідки ми починаємо малювати
RoundedBox
@Composable
fun RoundedBoxLeft(rotationAngle: Float, hazeState: HazeState, hazeStyle: HazeStyle, modifier: Modifier) {
Page(
hazeState = hazeState,
hazeStyle = hazeStyle,
borderColor = Color(0xFF24F4FE),
modifier = modifier.graphicsLayer {
rotationY = rotationAngle
rotationX = -45f
cameraDistance = 100f
transformOrigin = TransformOrigin(1f, 0f)
}
)
}
Основною різницею між лівою та правою частиною є transformOrigin = TransformOrigin(1f, 0f) та TransformOrigin(1f, 0f)
Ці блоки використовують graphicsLayer
для створення ефекту перспективи
Page
@Composable
fun Page(
hazeState: HazeState,
hazeStyle: HazeStyle,
borderColor: Color,
modifier: Modifier
) {
Box(
modifier = modifier
.hazeEffect(hazeState, style = hazeStyle)
.border(8.dp, borderColor)
)
}
Тут все просто. Box з обводкою та нашим ефектом скла. При необхідності можна задати заливки чи паралакс ефект
zIndex
У нашому прикладі є дуже важливим zIndex. Для лівої сторони ми його визначаємо як zIndex(rotationAngle)
для правої zIndex(360 - rotationAngle)

hazeSource
Для отримання ефекту розмиття спочатку потрібно отримати інформацію з картинки позаду. Тому ми hazeState
витягнули з PerplexityLogo
ззовні, де є наш бекграунд і за допомогою hazeSource отримуємо його. Обов’язково потрібно вказати zIndex = 0f
.
Box(
contentAlignment = Alignment.Center
) {
val hazeStateLeft = remember { HazeState() }
val hazeStateRight = remember { HazeState() }
Box(
modifier = Modifier
.offset(y = (-30).dp)
.size(300.dp)
.hazeSource(hazeStateRight, zIndex = 0f)
.hazeSource(hazeStateLeft, zIndex = 0f)
.graphicsLayer {
rotationZ = animationProgress * 1080
}
.clip(CircleShape)
.paint(
painter = painterResource(id = R.drawable.orb_bg),
contentScale = ContentScale.Crop
)
)
PerplexityLogo(
hazeStateLeft,
hazeStateRight,
modifier = Modifier.size(200.dp)
)
}
А в PerplexityLogo
ми вже вказуємо zIndex залежно від оберту: .hazeSource(hazeStateLeft, rotationAngle + 1f)
— за тим самим принципом, що й zIndex
, але з +1 (оскільки попередній шар був 0).
Результат
Цей код створює анімований логотип Perplexity з ефектом матового скла. Використовуючи HazeEffect
та graphicsLayer
, ми досягаємо красивого візуального ефекту з плавною анімацією.

🔗 Посилання на репозиторій з кодом: https://github.com/rmnkhr/perplexity_ui_experiment
🔗 Посилання на канал по Android в тг
https://t.me/android_fragment