feat(ui): дизайн-система radiOLA — палитра, тема, типографика, бренд, motion, pill таб-бар
- цветовые токены тёмно-зелёной темы + RadiolaColors (CompositionLocal) - darkColorScheme + всегда тёмная тема, фирменные shapes - типографика с весами/размерами под макет - Brand: AppMark (градиентный R), RadiolaWordmark, MonoMark - Motion: спеки движения, pressScale, живой эквалайзер - pill-таб-бар с анимированной активной вкладкой
This commit is contained in:
102
app/src/main/java/com/radiola/ui/theme/Motion.kt
Normal file
102
app/src/main/java/com/radiola/ui/theme/Motion.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user