Skip to content

Instantly share code, notes, and snippets.

@Kyriakos-Georgiopoulos
Created March 26, 2026 16:12
Show Gist options
  • Select an option

  • Save Kyriakos-Georgiopoulos/0109b73939638db11a6c624470e007bb to your computer and use it in GitHub Desktop.

Select an option

Save Kyriakos-Georgiopoulos/0109b73939638db11a6c624470e007bb to your computer and use it in GitHub Desktop.
/*
* Copyright 2026 Kyriakos Georgiopoulos
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.graphics.Matrix
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
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.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.GlobalPositionAwareModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.invalidateDraw
import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.isActive
import kotlin.math.cos
import kotlin.math.hypot
import kotlin.math.sin
/**
* Immutable configuration for a shimmer effect.
*
* [colors] must contain at least two stops. Arrays derived from the color list are eagerly
* precomputed so the draw phase does not allocate on every frame.
*/
@Immutable
data class ShimmerTheme(
val colors: List<Color>,
val durationMillis: Int = 2_000,
val angle: Float = 20f,
) {
internal val argbColors: IntArray = colors.map(Color::toArgb).toIntArray()
internal val colorPositions: FloatArray = FloatArray(colors.size) { index ->
index / (colors.size - 1).toFloat()
}
init {
require(colors.size >= 2) { "ShimmerTheme requires at least 2 colors." }
}
}
/** Common shimmer presets for demos and quick usage. */
object ShimmerThemes {
val DefaultLight = ShimmerTheme(
colors = listOf(Color(0xFFE2E8F0), Color(0xFFF1F5F9), Color(0xFFE2E8F0)),
durationMillis = 1_500,
)
val Cyberpunk = ShimmerTheme(
colors = listOf(
Color(0xFF0F172A),
Color(0xFF06B6D4),
Color(0xFFD946EF),
Color(0xFF0F172A)
),
durationMillis = 1_800,
angle = 20f,
)
val DataSpace = ShimmerTheme(
colors = listOf(
Color(0xFF0F172A),
Color(0xFF3B82F6),
Color(0xFF2DD4BF),
Color(0xFF0F172A),
),
durationMillis = 1_800,
angle = 30f,
)
val SpatialSilver = ShimmerTheme(
colors = listOf(
Color(0xFFD1D5DB),
Color(0xFFE2E8F0),
Color(0xFFFFFFFF),
Color(0xFFD1D5DB),
),
durationMillis = 1_800,
angle = 10f,
)
val NeoGlass = ShimmerTheme(
colors = listOf(
Color(0xFFE2E8F0),
Color(0xFFE0F2FE),
Color(0xFFFDE68A),
Color(0xFFE9D5FF),
Color(0xFFE2E8F0),
),
durationMillis = 1_800,
angle = 18f,
)
}
/**
* Shared animation state for all shimmer nodes under a [ShimmerProvider].
*
* A single clock is intentionally shared across the subtree so multiple placeholders animate in
* sync without starting one coroutine per composable.
*/
@Stable
class ShimmerHostState(
val theme: ShimmerTheme,
val waveSize: Float,
) {
var activeNodeCount by mutableIntStateOf(0)
var progress by mutableFloatStateOf(0f)
}
private val LocalShimmerHostState = compositionLocalOf<ShimmerHostState?> { null }
/**
* Provides a shared shimmer clock to all descendant [Modifier.shimmer] calls.
*
* The animation only runs while at least one attached shimmer node is visible and loading.
* Descendants can still provide a [themeOverride] per modifier while reusing the same clock.
*/
@Composable
fun ShimmerProvider(
theme: ShimmerTheme = ShimmerThemes.DefaultLight,
content: @Composable () -> Unit,
) {
val configuration = LocalConfiguration.current
val density = LocalDensity.current
val screenWidthPx = with(density) { configuration.screenWidthDp.dp.toPx() }
val screenHeightPx = with(density) { configuration.screenHeightDp.dp.toPx() }
val waveSize = remember(screenWidthPx, screenHeightPx) {
hypot(screenWidthPx, screenHeightPx) * 2f
}
val hostState = remember(theme, waveSize) {
ShimmerHostState(theme = theme, waveSize = waveSize)
}
val isRunning = hostState.activeNodeCount > 0
LaunchedEffect(isRunning, theme.durationMillis) {
if (!isRunning) return@LaunchedEffect
val durationNanos = theme.durationMillis * 1_000_000L
var playTimeNanos = (hostState.progress * durationNanos).toLong()
var lastFrameTimeNanos = withFrameNanos { it }
while (isActive) {
withFrameNanos { frameTimeNanos ->
val deltaNanos = frameTimeNanos - lastFrameTimeNanos
lastFrameTimeNanos = frameTimeNanos
playTimeNanos += deltaNanos
hostState.progress = (playTimeNanos % durationNanos) / durationNanos.toFloat()
}
}
}
CompositionLocalProvider(
LocalShimmerHostState provides hostState,
content = content,
)
}
/**
* Draws a shimmer placeholder on top of the node while [visible] is true.
*
* When used inside [ShimmerProvider], the modifier participates in the shared animation clock.
* When used outside of a provider, the placeholder still renders with a static first frame.
*/
fun Modifier.shimmer(
visible: Boolean,
shape: Shape,
themeOverride: ShimmerTheme? = null,
): Modifier = this then ShimmerElement(
visible = visible,
shape = shape,
themeOverride = themeOverride,
)
private data class ShimmerElement(
val visible: Boolean,
val shape: Shape,
val themeOverride: ShimmerTheme?,
) : ModifierNodeElement<ShimmerNode>() {
override fun create(): ShimmerNode = ShimmerNode(
visible = visible,
shape = shape,
themeOverride = themeOverride,
)
override fun update(node: ShimmerNode) {
node.update(
visible = visible,
shape = shape,
themeOverride = themeOverride,
)
}
override fun androidx.compose.ui.platform.InspectorInfo.inspectableProperties() {
name = "shimmer"
properties["visible"] = visible
properties["shape"] = shape
properties["themeOverride"] = themeOverride
}
}
/**
* Draw node for the shimmer placeholder.
*
* The node caches shader-related objects and shape outlines to avoid per-frame allocations.
* Global position is tracked so the gradient appears to travel across the full window instead of
* resetting for each individual placeholder.
*/
private class ShimmerNode(
var visible: Boolean,
var shape: Shape,
var themeOverride: ShimmerTheme?,
) : Modifier.Node(),
DrawModifierNode,
CompositionLocalConsumerModifierNode,
GlobalPositionAwareModifierNode,
ObserverModifierNode {
private val shaderMatrix = Matrix()
private val paint = Paint()
private var globalX = 0f
private var globalY = 0f
private var cachedOutline: Outline? = null
private var cachedSize: Size = Size.Unspecified
private var cachedLayoutDirection: LayoutDirection? = null
private var cachedShape: Shape? = null
private var cachedTheme: ShimmerTheme? = null
private var attachedHost: ShimmerHostState? = null
override fun onAttach() {
updateHostRegistration()
}
override fun onDetach() {
attachedHost?.let { it.activeNodeCount -= 1 }
attachedHost = null
}
override fun onObservedReadsChanged() {
updateHostRegistration()
}
fun update(
visible: Boolean,
shape: Shape,
themeOverride: ShimmerTheme?,
) {
val visibilityChanged = this.visible != visible
this.visible = visible
this.shape = shape
this.themeOverride = themeOverride
if (visibilityChanged) {
updateHostRegistration()
}
invalidateDraw()
}
private fun updateHostRegistration() {
observeReads {
val newHost = currentValueOf(LocalShimmerHostState)
if (!visible) {
attachedHost?.let { it.activeNodeCount -= 1 }
attachedHost = null
return@observeReads
}
if (newHost != attachedHost) {
attachedHost?.let { it.activeNodeCount -= 1 }
attachedHost = newHost
attachedHost?.let { it.activeNodeCount += 1 }
}
}
}
override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
val position = coordinates.positionInWindow()
globalX = position.x
globalY = position.y
}
override fun ContentDrawScope.draw() {
if (!visible) {
drawContent()
return
}
val host = attachedHost
val activeTheme = themeOverride ?: host?.theme
if (activeTheme == null) {
drawContent()
return
}
val waveSize = host?.waveSize ?: hypot(size.width, size.height) * 2f
val progress = host?.progress ?: 0f
if (cachedSize != size || cachedTheme != activeTheme) {
createShader(theme = activeTheme, waveSize = waveSize)
cachedSize = size
cachedTheme = activeTheme
}
if (
cachedOutline == null ||
cachedSize != size ||
cachedLayoutDirection != layoutDirection ||
cachedShape != shape
) {
cachedOutline = shape.createOutline(size, layoutDirection, this)
cachedLayoutDirection = layoutDirection
cachedShape = shape
}
val translation = (progress * waveSize) - (waveSize / 2f)
shaderMatrix.reset()
shaderMatrix.setRotate(activeTheme.angle)
shaderMatrix.postTranslate(
translation - globalX,
-globalY,
)
// Re-applying the shader after its local matrix changes avoids stale internal caches on
// some older Android versions.
(paint.shader as? android.graphics.Shader)?.let { shader ->
shader.setLocalMatrix(shaderMatrix)
paint.shader = shader
}
drawIntoCanvas { canvas ->
canvas.drawOutline(cachedOutline!!, paint)
}
}
private fun createShader(
theme: ShimmerTheme,
waveSize: Float,
) {
val waveWidth = waveSize / 2f
paint.shader = android.graphics.LinearGradient(
0f,
0f,
waveWidth,
0f,
theme.argbColors,
theme.colorPositions,
android.graphics.Shader.TileMode.CLAMP,
)
}
}
/**
* Animated dark holographic background used by showcase cards after loading completes.
*/
fun Modifier.animatedDarkHoloBackground(
enabled: Boolean,
shape: Shape,
): Modifier = composed {
if (!enabled) {
return@composed this.background(Color(0xFF1E293B), shape)
}
val transition = rememberInfiniteTransition(label = "dark_holo_background")
val angle = transition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(4_000, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "dark_holo_angle",
)
drawWithCache {
val outline = shape.createOutline(size, layoutDirection, this)
val radius = hypot(size.width, size.height)
onDrawBehind {
val angleRadians = Math.toRadians(angle.value.toDouble())
val brush = Brush.linearGradient(
colors = listOf(Color(0xFFD946EF), Color(0xFF06B6D4), Color(0xFF3B82F6)),
start = Offset.Zero,
end = Offset(
x = (cos(angleRadians) * radius).toFloat(),
y = (sin(angleRadians) * radius).toFloat(),
),
)
drawOutline(outline = outline, brush = brush)
}
}
}
/**
* Animated light holographic background used by the commerce hero once content is loaded.
*
* Two animations are combined intentionally:
* - rotation keeps the background moving across large surfaces,
* - alpha shifting adds a subtle premium "glass" feel without introducing another gradient pass.
*/
fun Modifier.animatedNeoGlassBackground(
enabled: Boolean,
shape: Shape,
): Modifier = composed {
if (!enabled) {
return@composed this.background(Color(0xFFF8FAFC), shape)
}
val transition = rememberInfiniteTransition(label = "neo_glass_background")
val angle = transition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(6_000, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "neo_glass_angle",
)
val highlightShift = transition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(4_000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse,
),
label = "neo_glass_highlight_shift",
)
drawWithCache {
val outline = shape.createOutline(size, layoutDirection, this)
val radius = hypot(size.width, size.height)
onDrawBehind {
val angleRadians = Math.toRadians(angle.value.toDouble())
val brush = Brush.linearGradient(
colors = listOf(
Color(0xFFE0F2FE).copy(alpha = 0.9f),
Color(0xFFFDE68A).copy(alpha = 0.7f + (highlightShift.value * 0.2f)),
Color(0xFFE9D5FF).copy(alpha = 0.8f),
Color(0xFFF8FAFC),
),
start = Offset.Zero,
end = Offset(
x = (cos(angleRadians) * radius).toFloat(),
y = (sin(angleRadians) * radius).toFloat(),
),
)
drawOutline(outline = outline, brush = brush)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShimmerShowcaseScreen() {
var isLoading by remember { mutableStateOf(true) }
val pagerState = rememberPagerState(pageCount = { 4 })
val backgroundColors = listOf(
Color(0xFF0B0F19),
Color(0xFF050B14),
Color(0xFFF8FAFC),
Color(0xFFFAFAFA),
)
val animatedBackgroundColor by animateColorAsState(
targetValue = backgroundColors[pagerState.currentPage],
animationSpec = tween(600),
label = "showcase_background",
)
ShimmerProvider(theme = ShimmerThemes.Cyberpunk) {
Scaffold(
containerColor = animatedBackgroundColor,
bottomBar = { ShowcasePagerIndicator(pagerState = pagerState, pageCount = 4) },
) { innerPadding ->
HorizontalPager(
state = pagerState,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
) { page ->
when (page) {
0 -> AnalyticsDashboardScreen(
isLoading = isLoading,
theme = ShimmerThemes.Cyberpunk
)
1 -> DiagramScreen(isLoading = isLoading, theme = ShimmerThemes.DataSpace)
2 -> SpatialSmartHomeScreen(
isLoading = isLoading,
theme = ShimmerThemes.SpatialSilver
)
3 -> MinimalCommerceScreen(
isLoading = isLoading,
theme = ShimmerThemes.NeoGlass
)
}
}
}
}
}
@Composable
fun ShowcasePagerIndicator(
pagerState: PagerState,
pageCount: Int,
) {
val activeColors = listOf(
Color(0xFF06B6D4),
Color(0xFF10B981),
Color(0xFFF59E0B),
Color(0xFF8B5CF6),
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
repeat(pageCount) { index ->
val isSelected = pagerState.currentPage == index
val width by animateDpAsState(
targetValue = if (isSelected) 32.dp else 8.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow,
),
label = "indicator_width_$index",
)
val inactiveColor = if (pagerState.currentPage >= 2) {
Color(0xFFCBD5E1)
} else {
Color(0xFF334155)
}
val color by animateColorAsState(
targetValue = if (isSelected) activeColors[index] else inactiveColor,
animationSpec = spring(stiffness = Spring.StiffnessLow),
label = "indicator_color_$index",
)
Box(
modifier = Modifier
.padding(horizontal = 6.dp)
.height(8.dp)
.width(width)
.background(color, CircleShape),
)
}
}
}
@Composable
fun AnalyticsDashboardScreen(
isLoading: Boolean,
theme: ShimmerTheme,
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(56.dp)
.shimmer(visible = isLoading, shape = CircleShape, themeOverride = theme)
.background(Color(0xFF1E293B), CircleShape),
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Box(
modifier = Modifier
.width(120.dp)
.height(18.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.width(80.dp)
.height(14.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
}
}
Box(
modifier = Modifier
.size(48.dp)
.shimmer(visible = isLoading, shape = CircleShape, themeOverride = theme)
.background(Color(0xFF1E293B), CircleShape),
)
}
Spacer(modifier = Modifier.height(40.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(250.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(24.dp),
themeOverride = theme
)
.animatedDarkHoloBackground(
enabled = !isLoading,
shape = RoundedCornerShape(24.dp)
),
)
Spacer(modifier = Modifier.height(32.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
repeat(3) {
Box(
modifier = Modifier
.weight(1f)
.height(80.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(16.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B), RoundedCornerShape(16.dp)),
)
}
}
Spacer(modifier = Modifier.height(40.dp))
Box(
modifier = Modifier
.width(140.dp)
.height(20.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
Spacer(modifier = Modifier.height(24.dp))
repeat(5) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 20.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(48.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(12.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B), RoundedCornerShape(12.dp)),
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Box(
modifier = Modifier
.fillMaxWidth(0.6f)
.height(16.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.fillMaxWidth(0.3f)
.height(12.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
}
Box(
modifier = Modifier
.width(60.dp)
.height(16.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
}
}
}
}
@Composable
fun DiagramScreen(
isLoading: Boolean,
theme: ShimmerTheme,
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column {
Box(
modifier = Modifier
.width(160.dp)
.height(24.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(6.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.width(100.dp)
.height(12.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
}
Box(
modifier = Modifier
.width(80.dp)
.height(40.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(20.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B), RoundedCornerShape(20.dp)),
)
}
Spacer(modifier = Modifier.height(40.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(250.dp)
.background(Color(0xFF1E293B), RoundedCornerShape(24.dp)),
contentAlignment = Alignment.Center,
) {
Box(
modifier = Modifier
.size(160.dp)
.shimmer(visible = isLoading, shape = CircleShape, themeOverride = theme)
.background(Color(0xFF334155), CircleShape),
)
Box(
modifier = Modifier
.size(100.dp)
.background(Color(0xFF1E293B), CircleShape),
)
Box(
modifier = Modifier
.width(48.dp)
.height(20.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF475569)),
)
}
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
.background(Color(0xFF1E293B), RoundedCornerShape(24.dp))
.padding(24.dp),
) {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom,
) {
val heights = listOf(0.4f, 0.7f, 0.5f, 1.0f, 0.3f, 0.8f)
heights.forEach { heightFraction ->
Box(
modifier = Modifier
.width(32.dp)
.fillMaxHeight(heightFraction)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp),
themeOverride = theme,
)
.background(
Color(0xFF334155),
RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp),
),
)
}
}
}
Spacer(modifier = Modifier.height(40.dp))
Box(
modifier = Modifier
.width(120.dp)
.height(20.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
Spacer(modifier = Modifier.height(24.dp))
repeat(4) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(12.dp)
.shimmer(visible = isLoading, shape = CircleShape, themeOverride = theme)
.background(Color(0xFF1E293B), CircleShape),
)
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.weight(1f)
.height(16.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
Spacer(modifier = Modifier.width(24.dp))
Box(
modifier = Modifier
.width(80.dp)
.height(8.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(Color(0xFF1E293B)),
)
}
}
}
}
@Composable
fun SpatialSmartHomeScreen(
isLoading: Boolean,
theme: ShimmerTheme,
) {
val loadedBaseColor = Color(0xFFE2E8F0)
val loadedInnerColor = Color(0xFFCBD5E1)
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column {
Box(
modifier = Modifier
.width(140.dp)
.height(28.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(8.dp),
themeOverride = theme
)
.background(loadedBaseColor),
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.width(200.dp)
.height(14.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(loadedBaseColor),
)
}
Row(horizontalArrangement = Arrangement.spacedBy((-12).dp)) {
repeat(3) {
Box(
modifier = Modifier
.size(48.dp)
.background(Color(0xFFF8FAFC), CircleShape)
.padding(2.dp)
.shimmer(
visible = isLoading,
shape = CircleShape,
themeOverride = theme
)
.background(loadedBaseColor, CircleShape),
)
}
}
}
Spacer(modifier = Modifier.height(40.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(220.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Box(
modifier = Modifier
.weight(1.5f)
.fillMaxHeight()
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(32.dp),
themeOverride = theme
)
.background(loadedBaseColor, RoundedCornerShape(32.dp)),
)
Column(
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(24.dp),
themeOverride = theme
)
.background(loadedBaseColor, RoundedCornerShape(24.dp)),
)
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(24.dp),
themeOverride = theme
)
.background(loadedBaseColor, RoundedCornerShape(24.dp)),
)
}
}
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(24.dp),
themeOverride = theme
)
.background(loadedBaseColor, RoundedCornerShape(24.dp))
.padding(16.dp),
) {
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(64.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(12.dp),
themeOverride = theme
)
.background(loadedInnerColor, RoundedCornerShape(12.dp)),
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Box(
modifier = Modifier
.fillMaxWidth(0.7f)
.height(16.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(loadedInnerColor),
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.fillMaxWidth(0.4f)
.height(12.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(loadedInnerColor),
)
Spacer(modifier = Modifier.height(12.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.shimmer(
visible = isLoading,
shape = CircleShape,
themeOverride = theme
)
.background(loadedInnerColor),
)
}
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.size(40.dp)
.shimmer(visible = isLoading, shape = CircleShape, themeOverride = theme)
.background(loadedInnerColor, CircleShape),
)
}
}
Spacer(modifier = Modifier.height(40.dp))
Box(
modifier = Modifier
.width(160.dp)
.height(20.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(6.dp),
themeOverride = theme
)
.background(loadedBaseColor),
)
Spacer(modifier = Modifier.height(24.dp))
val placeholders = List(5) { it }
LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
items(placeholders) {
Box(
modifier = Modifier
.width(80.dp)
.height(140.dp)
.shimmer(visible = isLoading, shape = CircleShape, themeOverride = theme)
.background(loadedBaseColor, CircleShape),
) {
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 16.dp)
.size(32.dp)
.shimmer(
visible = isLoading,
shape = CircleShape,
themeOverride = theme
)
.background(loadedInnerColor, CircleShape),
)
}
}
}
}
}
@Composable
fun MinimalCommerceScreen(
isLoading: Boolean,
theme: ShimmerTheme,
) {
val loadedBaseColor = Color(0xFFF8FAFC)
val loadedInnerColor = Color(0xFFE2E8F0)
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(48.dp)
.shimmer(visible = isLoading, shape = CircleShape, themeOverride = theme)
.background(loadedBaseColor, CircleShape),
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Box(
modifier = Modifier
.width(100.dp)
.height(14.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(loadedBaseColor),
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.width(60.dp)
.height(12.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(loadedBaseColor),
)
}
}
Box(
modifier = Modifier
.size(40.dp)
.shimmer(visible = isLoading, shape = CircleShape, themeOverride = theme)
.background(loadedBaseColor, CircleShape),
)
}
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(16.dp),
themeOverride = theme
)
.background(loadedBaseColor, RoundedCornerShape(16.dp)),
)
Spacer(modifier = Modifier.height(24.dp))
Box(
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(24.dp),
themeOverride = theme
)
.animatedNeoGlassBackground(
enabled = !isLoading,
shape = RoundedCornerShape(24.dp)
),
)
Spacer(modifier = Modifier.height(32.dp))
val categories = List(5) { it }
LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
items(categories) {
Box(
modifier = Modifier
.width(80.dp)
.height(36.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(24.dp),
themeOverride = theme
)
.background(loadedBaseColor, RoundedCornerShape(24.dp)),
)
}
}
Spacer(modifier = Modifier.height(32.dp))
Box(
modifier = Modifier
.width(140.dp)
.height(20.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(6.dp),
themeOverride = theme
)
.background(loadedBaseColor),
)
Spacer(modifier = Modifier.height(24.dp))
repeat(2) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
repeat(2) {
Column(modifier = Modifier.weight(1f)) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(160.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(16.dp),
themeOverride = theme
)
.background(loadedBaseColor, RoundedCornerShape(16.dp)),
)
Spacer(modifier = Modifier.height(12.dp))
Box(
modifier = Modifier
.fillMaxWidth(0.8f)
.height(14.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(loadedInnerColor),
)
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.fillMaxWidth(0.4f)
.height(14.dp)
.shimmer(
visible = isLoading,
shape = RoundedCornerShape(4.dp),
themeOverride = theme
)
.background(loadedInnerColor),
)
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment