Theme Picker Animation in Jetpack Compose | by sinasamaki | Feb, 2022

Offer customized Android themes to your users

sinasamaki
data class CustomTheme(  
val primaryColor: Color,
val background: Color,
val textColor: Color,
val image: Int,
)

val darkTheme = CustomTheme(
primaryColor = Color(0xFFE9B518),
background = Color(0xFF111111),
textColor = Color(0xffFFFFFF),
image = R.drawable.dark,
)

val lightTheme = CustomTheme(
primaryColor = Color(0xFF2CB6DA),
background = Color(0xFFF1F1F1),
textColor = Color(0xff000000),
image = R.drawable.light,
)

val pinkTheme = CustomTheme(
primaryColor = Color(0xFFF01EE5),
background = Color(0xFF110910),
textColor = Color(0xFFEE8CE1),
image = R.drawable.pink,
)

@ExperimentalAnimationApi  
@Composable
fun App() {
var theme by remember { mutableStateOf(lightTheme) }
AnimatedContent(
targetState = theme,
modifier = Modifier
.background(Color.Black)
.fillMaxSize(),
) { currentTheme ->
Surface(
modifier = Modifier
.fillMaxSize(),
color = currentTheme.background
) {
Box {
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
) {
Image(
painter = painterResource(id = currentTheme.image),
contentDescription = "headerImage",
contentScale = ContentScale.Crop,
)
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent, currentTheme.background.copy(alpha = .2f),
currentTheme.background
)
)
)
)
}

Row(
modifier = Modifier
.align(Alignment.Center),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {

ThemeButton(
theme = lightTheme,
currentTheme = currentTheme,
text = "Light",
) {
theme = lightTheme
}

ThemeButton(
theme = darkTheme,
currentTheme = currentTheme,
text = "Dark",
) {
theme = darkTheme
}

ThemeButton(
theme = pinkTheme,
currentTheme = currentTheme,
text = "Pink",
) {
theme = pinkTheme
}
}
}
}
}
}

transitionSpec = {  
fadeIn(
initialAlpha = 0f,
animationSpec = tween(100)
) with fadeOut(
targetAlpha = .9f,
animationSpec = tween(800)
) + scaleOut(
targetScale = .95f,
animationSpec = tween(800)
)
}
...
var theme by remember { mutableStateOf(pinkTheme) }
var animationOffset by remember { mutableStateOf(Offset(0f, 0f)) }
AnimatedContent(
...
) { currentTheme ->
val revealSize = remember { Animatable(1f) }
LaunchedEffect(key1 = "reveal", block = {
if (animationOffset.x > 0f) {
revealSize.snapTo(0f)
revealSize.animateTo(1f, animationSpec = tween(800))
} else {
revealSize.snapTo(1f)
}
})

Box(
modifier = Modifier
.fillMaxSize()
.clip(CirclePath(revealSize.value, animationOffset))
) {
Surface(
...

class CirclePath(private val progress: Float, private val origin: Offset = Offset(0f, 0f)) : Shape {  
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {

val center = Offset(
x = size.center.x - ((size.center.x - origin.x) * (1f - progress)),
y = size.center.y - ((size.center.y - origin.y) * (1f - progress)),
)
val radius = (sqrt(
size.height * size.height + size.width * size.width
) * .5f) * progress

return Outline.Generic(
Path().apply {
addOval(
Rect(
center = center,
radius = radius,
)
)
}
)
}
}

@Composable  
fun ThemeButton(
theme: CustomTheme,
currentTheme: CustomTheme,
text: String,
onClick: (Offset) -> Unit,
) {
val isSelected = theme == currentTheme
var offset: Offset = remember { Offset(0f, 0f) }
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.onGloballyPositioned {
offset = Offset(
x = it.positionInWindow().x + it.size.width / 2,
y = it.positionInWindow().y + it.size.height / 2
)
}
.size(110.dp)
.border(
4.dp,
color = if (isSelected) theme.primaryColor else Color.Transparent,
shape = CircleShape
)
.padding(8.dp)
.background(color = theme.primaryColor, shape = CircleShape)
.clip(CircleShape)
.clickable {
onClick(offset)
}
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(id = theme.image),
contentDescription = "themeImage",
contentScale = ContentScale.Crop,
)
}

Text(
text = text.uppercase(),
modifier = Modifier
.alpha(if (isSelected) 1f else .5f)
.padding(2.dp),
color = currentTheme.textColor,
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
}
}

ThemeButton(  
...
) {
animationOffset = it
theme = lightTheme
}

ThemeButton(
...
) {
animationOffset = it
theme = darkTheme
}

ThemeButton(
...
) {
animationOffset = it
theme = pinkTheme
}

Leave a Comment