Skip to content

Instantly share code, notes, and snippets.

@matthewrkula
Created February 17, 2022 05:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matthewrkula/f5a0ff8ef7996aabb1ef66d9c3417041 to your computer and use it in GitHub Desktop.
Save matthewrkula/f5a0ff8ef7996aabb1ef66d9c3417041 to your computer and use it in GitHub Desktop.
A composable layout which transitions between two separate composables including their heights
// Variable Heights
package com.mattkula.layouts
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
@Preview
fun Preview() {
Blah()
}
sealed class PreviewWindowNode {
data class Message(val text: String) : PreviewWindowNode()
data class Advertisement(val text: String) : PreviewWindowNode()
}
val Nodes = listOf(
PreviewWindowNode.Message("Message One"),
PreviewWindowNode.Message("Message Two"),
PreviewWindowNode.Advertisement("This is an Ad"),
)
@Composable
fun Blah() {
var messageIndex by remember { mutableStateOf(0) }
Column(
Modifier
.fillMaxWidth()
.wrapContentHeight()
.clickable { messageIndex++ }
) {
ChatPreviewWindow(
nodeOne = Nodes[messageIndex % Nodes.size],
nodeTwo = Nodes[(messageIndex + 1) % Nodes.size],
modifier = Modifier.padding(16.dp)
.clip(RoundedCornerShape(8.dp))
// .height(60.dp)
.wrapContentHeight()
.fillMaxWidth()
)
}
}
@Composable
fun ChatPreviewWindow(
nodeOne: PreviewWindowNode,
nodeTwo: PreviewWindowNode,
modifier: Modifier = Modifier,
) {
val transitionPercent = remember(nodeTwo) { Animatable(0f) }
LaunchedEffect(nodeTwo) {
transitionPercent.animateTo(
1.0f,
animationSpec = tween(durationMillis = 400)
)
}
TransitionalLayout(
transitionPercent = transitionPercent.value,
modifier = modifier
) {
Node(nodeOne)
Node(nodeTwo)
}
}
@Composable
fun Node(node: PreviewWindowNode) {
when (node) {
is PreviewWindowNode.Message -> ChatMessage(text = node.text)
is PreviewWindowNode.Advertisement -> Advertisement(text = node.text)
}
}
enum class TransitionDirection {
UP,
DOWN,
}
@Composable
fun TransitionalLayout(
transitionPercent: Float,
modifier: Modifier = Modifier,
direction: TransitionDirection = TransitionDirection.DOWN,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier.clipToBounds(),
content = content,
) { measureables, constraints ->
assert(measureables.size <= 2) { "TransitionalLayout only supports 2 children" }
val placeables = measureables.map { it.measure(constraints) }
val (first, second) = if (placeables.size == 2) {
placeables.getOrNull(0) to placeables.getOrNull(1)
} else {
null to placeables.getOrNull(0)
}
val firstHeight = placeables[0].height
val secondHeight = placeables[1].height
val layoutHeight = firstHeight + ((placeables[1].height - placeables[0].height) * transitionPercent).toInt()
layout(constraints.maxWidth, layoutHeight) {
when (direction) {
TransitionDirection.UP -> {
val translationY = (firstHeight * transitionPercent).toInt()
first?.placeRelative(x = 0, y = -translationY)
second?.placeRelative(x = 0, y = firstHeight - translationY)
}
TransitionDirection.DOWN -> {
val translationY = (secondHeight * transitionPercent).toInt()
first?.placeRelative(x = 0, y = translationY)
second?.placeRelative(x = 0, y = translationY - secondHeight)
}
}
}
}
}
@Composable
fun ChatMessage(
text: String,
modifier: Modifier = Modifier,
) {
Row(
modifier
.fillMaxWidth()
.height(80.dp)
.background(Color.Black)
) {
Box(
Modifier
.padding(start = 16.dp, top = 8.dp)
.background(Color.Red, CircleShape)
.size(16.dp)
)
Text(
text = text,
color = Color.White,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 8.dp, start = 12.dp)
)
}
}
@Composable
fun Advertisement(
text: String,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = modifier
.fillMaxWidth()
.height(160.dp)
.background(Color.Gray)
) {
Text(
text = text,
color = Color.White,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight()
.align(Alignment.CenterVertically)
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment