- цветовые токены тёмно-зелёной темы + RadiolaColors (CompositionLocal) - darkColorScheme + всегда тёмная тема, фирменные shapes - типографика с весами/размерами под макет - Brand: AppMark (градиентный R), RadiolaWordmark, MonoMark - Motion: спеки движения, pressScale, живой эквалайзер - pill-таб-бар с анимированной активной вкладкой
103 lines
3.7 KiB
Kotlin
103 lines
3.7 KiB
Kotlin
package com.radiola.ui.theme
|
|
|
|
import androidx.compose.animation.core.Animatable
|
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
|
import androidx.compose.animation.core.LinearEasing
|
|
import androidx.compose.animation.core.RepeatMode
|
|
import androidx.compose.animation.core.Spring
|
|
import androidx.compose.animation.core.animateFloat
|
|
import androidx.compose.animation.core.infiniteRepeatable
|
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
|
import androidx.compose.animation.core.spring
|
|
import androidx.compose.animation.core.tween
|
|
import androidx.compose.foundation.Canvas
|
|
import androidx.compose.foundation.gestures.detectTapGestures
|
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.geometry.CornerRadius
|
|
import androidx.compose.ui.geometry.Offset
|
|
import androidx.compose.ui.geometry.Size
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.graphicsLayer
|
|
import androidx.compose.ui.unit.dp
|
|
import kotlin.math.abs
|
|
|
|
/** Стандартные длительности и кривые движения приложения. */
|
|
object Motion {
|
|
const val Fast = 120
|
|
const val Medium = 220
|
|
const val Slow = 360
|
|
|
|
fun <T> snappy() = spring<T>(
|
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
stiffness = Spring.StiffnessMediumLow
|
|
)
|
|
|
|
fun <T> gentle() = tween<T>(durationMillis = Medium, easing = FastOutSlowInEasing)
|
|
}
|
|
|
|
/** Лёгкое нажатие: плавное уменьшение масштаба, пока палец на элементе. */
|
|
@Composable
|
|
fun Modifier.pressScale(
|
|
pressedScale: Float = 0.94f,
|
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
|
): Modifier {
|
|
val pressed by interactionSource.collectIsPressedAsState()
|
|
val scale by androidx.compose.animation.core.animateFloatAsState(
|
|
targetValue = if (pressed) pressedScale else 1f,
|
|
animationSpec = tween(Motion.Fast, easing = FastOutSlowInEasing),
|
|
label = "pressScale"
|
|
)
|
|
return this.graphicsLayer {
|
|
scaleX = scale
|
|
scaleY = scale
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Живой эквалайзер для прямого эфира — декоративный, без перемотки.
|
|
* Полоски плавно «дышат» при воспроизведении.
|
|
*/
|
|
@Composable
|
|
fun LiveEqualizer(
|
|
modifier: Modifier = Modifier,
|
|
barCount: Int = 36,
|
|
color: Color = Accent,
|
|
playing: Boolean = true
|
|
) {
|
|
val transition = rememberInfiniteTransition(label = "eq")
|
|
val phase by transition.animateFloat(
|
|
initialValue = 0f,
|
|
targetValue = (Math.PI * 2).toFloat(),
|
|
animationSpec = infiniteRepeatable(
|
|
animation = tween(1400, easing = LinearEasing),
|
|
repeatMode = RepeatMode.Restart
|
|
),
|
|
label = "eqPhase"
|
|
)
|
|
Canvas(modifier = modifier) {
|
|
val gap = 3.dp.toPx()
|
|
val barWidth = (size.width - gap * (barCount - 1)) / barCount
|
|
val maxH = size.height
|
|
for (i in 0 until barCount) {
|
|
val seed = (i * 0.7f)
|
|
val wave = if (playing) {
|
|
0.45f + 0.55f * abs(kotlin.math.sin(phase + seed))
|
|
} else 0.25f
|
|
val h = maxH * wave
|
|
val x = i * (barWidth + gap)
|
|
val y = (maxH - h) / 2f
|
|
drawRoundRect(
|
|
color = color,
|
|
topLeft = Offset(x, y),
|
|
size = Size(barWidth, h),
|
|
cornerRadius = CornerRadius(barWidth / 2f, barWidth / 2f)
|
|
)
|
|
}
|
|
}
|
|
}
|