Skip to content

Instantly share code, notes, and snippets.

@Bruno125
Created October 26, 2020 05:10
Show Gist options
  • Save Bruno125/b1a87d80805c91c105c5827310e716ad to your computer and use it in GitHub Desktop.
Save Bruno125/b1a87d80805c91c105c5827310e716ad to your computer and use it in GitHub Desktop.
import androidx.compose.animation.RectPropKey
import androidx.compose.animation.core.FloatPropKey
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.transitionDefinition
import androidx.compose.animation.core.tween
import androidx.compose.animation.transition
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.Text
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChatBubble
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawOpacity
import androidx.compose.ui.draw.drawShadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Radius
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.DensityAmbient
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.ui.tooling.preview.Preview
private val circleSize = 60.dp
private val expandedSize = 300.dp
enum class IndicatorState { Collapsed, Expanded }
data class AnimationProperties(
val size: RectPropKey,
val colors: List<Color>,
val colorSizes: List<RectPropKey>,
val elevation: FloatPropKey,
val contentAlpha: FloatPropKey
)
fun createProperties(colors: List<Color>): AnimationProperties {
return AnimationProperties(
size = RectPropKey("Container size"),
elevation = FloatPropKey("Elevation"),
colors = colors,
colorSizes = colors.mapIndexed { index, _ ->
RectPropKey("Animated color size $index")
},
contentAlpha = FloatPropKey("Content alpha")
)
}
fun rect(w: Float, h: Float) = Rect(Offset(0), Size(w, h))
fun colorsTransition(
properties: AnimationProperties,
width: Float,
height: Float,
) = transitionDefinition<IndicatorState> {
val zero = rect(0f, 0f)
val expanded = rect(width, height)
state(IndicatorState.Collapsed) {
this[properties.size] = zero
this[properties.elevation] = 0f
this[properties.contentAlpha] = 0f
properties.colorSizes.forEach { this[it] = zero }
}
state(IndicatorState.Expanded) {
this[properties.size] = expanded
this[properties.elevation] = 32f
this[properties.contentAlpha] = 1f
properties.colorSizes.forEach { this[it] = expanded }
}
val n = properties.colors.size
val breakpoints = (1..n+2).map { 150 * it }
val offset = 100
transition(fromState = IndicatorState.Collapsed, toState = IndicatorState.Expanded) {
properties.elevation using keyframes { 0f at breakpoints[0] }
properties.size using keyframes {
rect(w = height, h = height) at breakpoints[0]
durationMillis = breakpoints[1]
}
properties.colorSizes.forEachIndexed { index, sizeProperty ->
sizeProperty using keyframes {
val start = index + 2
var remaining = start
while(remaining > 0) {
val size = height / remaining
val time = breakpoints[start - remaining] - if(remaining != start) offset else 0
rect(size, size) at time
remaining -= 1
}
durationMillis = breakpoints[start]
}
}
properties.contentAlpha using tween(
delayMillis = breakpoints.last(),
durationMillis = 300
)
}
}
@Composable
fun RainbowLayout(
modifier: Modifier = Modifier,
background: Color = Color.White,
animatedColors: List<Color> = emptyList(),
content: @Composable RowScope.()->Unit = { }
) {
val circleSizePx = with(DensityAmbient.current) { circleSize.toPx() }
val expandedSizePx = with(DensityAmbient.current) { expandedSize.toPx() }
val properties = remember { createProperties(animatedColors) }
val transition = transition(
definition = remember { colorsTransition(properties, expandedSizePx, circleSizePx) },
initState = IndicatorState.Collapsed,
toState = IndicatorState.Expanded
)
val contentSize = transition[properties.size].size
val contentSizeW = with(DensityAmbient.current) { contentSize.width.toDp() }
val contentSizeH = with(DensityAmbient.current) { contentSize.height.toDp() }
val shape = RoundedCornerShape(50)
Box(modifier
.width(contentSizeW.coerceAtLeast(circleSize))
.height(contentSizeH.coerceAtLeast(circleSize))
.drawShadow(elevation = Dp(transition[properties.elevation]), shape = shape)
) {
Canvas(Modifier
.align(Alignment.Center)
.width(contentSizeW)
.height(contentSizeH)
.clip(shape)
.background(background)
) {
fun centerFor(rect: Size) = Offset(center.x - rect.width / 2, center.y - rect.height / 2)
fun radiusFor(rect: Size) = Radius(rect.height / 2)
animatedColors.forEachIndexed { i, color ->
val size = transition[properties.colorSizes[i]].size
drawRoundRect(
color = color,
topLeft = centerFor(size),
size = size,
radius = radiusFor(size)
)
}
}
val contentAlpha = transition[properties.contentAlpha]
if(contentAlpha != 0f) {
Row(
modifier = Modifier.fillMaxSize().drawOpacity(contentAlpha),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
content()
}
}
}
}
@Preview
@Composable
private fun RainbowLayoutPreview() {
Surface(Modifier.fillMaxSize(), color = Color.White) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(Modifier.height(8.dp))
RainbowLayout(animatedColors = listOf(
Color(0xFF820263),
Color(0xFFD90368),
Color(0xFFEADEDA),
)) {
Image(asset = Icons.Filled.ChatBubble)
Spacer(Modifier.width(16.dp))
Text("Chat")
}
Spacer(Modifier.height(8.dp))
RainbowLayout(
background = Color(0xFF2C2846),
animatedColors = listOf(
Color(0xFF3A726F),
Color(0xFF88B387),
Color.White
)
) {
Text("Test 1")
}
Spacer(Modifier.height(8.dp))
RainbowLayout(animatedColors = listOf(
Color(0xFF684551),
Color(0xFFD5B0AC),
Color(0xFFCEA0AE),
)) {
Text("Test 2")
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment