Skip to content

Instantly share code, notes, and snippets.

@KlassenKonstantin
Last active May 1, 2024 11:54
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save KlassenKonstantin/502ab8969124c073812531533418e329 to your computer and use it in GitHub Desktop.
Save KlassenKonstantin/502ab8969124c073812531533418e329 to your computer and use it in GitHub Desktop.
List sections
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
package de.kuno.listsections
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateRectAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import de.kuno.listsections.ListElement.Role.BOTTOM
import de.kuno.listsections.ListElement.Role.MIDDLE
import de.kuno.listsections.ListElement.Role.SINGLE
import de.kuno.listsections.ListElement.Role.TOP
import de.kuno.listsections.ui.theme.ListSectionsTheme
import java.util.UUID
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ListSectionsTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
val todos = remember {
mutableStateListOf(
Todo(text = "Foo", isChecked = false),
Todo(text = "Bar", isChecked = false),
Todo(text = "Bauz", isChecked = false),
Todo(text = "Baz", isChecked = false),
)
}
val listItems = todos.toSections().toListElements()
val radioOptions = listOf("Merged", "Divided", "Rounded")
val (selectedOption, onOptionSelected) = remember { mutableIntStateOf(0) }
val spacedBy by animateDpAsState(Dp(selectedOption * 2f))
val innerCornerSize by animateDpAsState(Dp(selectedOption * 4f))
Column {
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(spacedBy, Alignment.CenterVertically)
) {
items(
items = listItems,
key = { it.id },
contentType = { it::class }
) {
when (it) {
is ListElement.Header -> HeaderItem(
modifier = Modifier.animateItemPlacement(),
text = it
)
is ListElement.Item -> TodoItem(
modifier = Modifier.animateItemPlacement(),
todoItem = it,
innerCornerSize = innerCornerSize
) { clickedId ->
val index = todos.indexOf(todos.find { it.id == clickedId })
if (index >= 0) {
todos[index] = todos[index].copy(isChecked = !todos[index].isChecked)
}
}
}
}
}
Column(Modifier.selectableGroup()) {
radioOptions.forEachIndexed { index, text ->
Row(
Modifier
.fillMaxWidth()
.height(56.dp)
.selectable(
selected = (index == selectedOption),
onClick = { onOptionSelected(index) },
role = androidx.compose.ui.semantics.Role.RadioButton
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (index == selectedOption),
onClick = null
)
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
}
}
}
}
}
@Composable
fun HeaderItem(text: ListElement.Header, modifier: Modifier = Modifier) {
Text(
modifier = modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp, bottom = 8.dp),
text = text.text,
style = MaterialTheme.typography.titleMedium
)
}
@Composable
fun TodoItem(
todoItem: ListElement.Item,
modifier: Modifier = Modifier,
outerCornerSize: Dp = 20.dp,
innerCornerSize: Dp = 0.dp,
onClick: (String) -> Unit
) {
val shape = todoItem.role.toShape(
outerCornerSize = outerCornerSize,
innerCornerSize = innerCornerSize
)
Card(
modifier = modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
shape = shape,
onClick = { onClick(todoItem.todo.id) }
) {
Row(
modifier = Modifier
.heightIn(min = 56.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f),
text = todoItem.todo.text,
style = MaterialTheme.typography.bodyLarge,
)
Checkbox(checked = todoItem.todo.isChecked, onCheckedChange = null)
}
}
}
@Composable
private fun ListElement.Role.toShape(outerCornerSize: Dp, innerCornerSize: Dp): Shape {
val (outerCornerSizePx, innerCornerSizePx) = LocalDensity.current.run {
outerCornerSize.toPx() to innerCornerSize.toPx()
}
val targetRect = remember(this, outerCornerSize, innerCornerSize) {
when (this) {
TOP -> Rect(outerCornerSizePx, outerCornerSizePx, innerCornerSizePx, innerCornerSizePx)
BOTTOM -> Rect(innerCornerSizePx, innerCornerSizePx, outerCornerSizePx, outerCornerSizePx)
MIDDLE -> Rect(innerCornerSizePx, innerCornerSizePx, innerCornerSizePx, innerCornerSizePx)
SINGLE -> Rect(outerCornerSizePx, outerCornerSizePx, outerCornerSizePx, outerCornerSizePx)
}
}
val animatedRect by animateRectAsState(targetRect)
return RoundedCornerShape(
animatedRect.left, animatedRect.top, animatedRect.right, animatedRect.bottom
)
}
data class Todo(
val id: String = UUID.randomUUID().toString(),
val text: String,
val isChecked: Boolean,
)
data class Section(
val header: String?,
private val todos: List<Todo>
) {
val todosWithRoles = todos.associateWith { todo ->
when {
todos.size == 1 -> SINGLE
todos.indexOf(todo) == 0 -> TOP
todos.indexOf(todo) == todos.size - 1 -> BOTTOM
else -> MIDDLE
}
}
}
sealed interface ListElement {
val id: String
data class Header(
val text: String
) : ListElement {
override val id = text
}
data class Item(
val todo: Todo,
val role: Role
) : ListElement {
override val id = todo.id
}
enum class Role {
TOP, BOTTOM, MIDDLE, SINGLE
}
}
private fun List<Todo>.toSections(): List<Section> {
val (checkedTodos, uncheckedTodos) = partition { it.isChecked }
return buildList {
add(Section(null, uncheckedTodos))
if (checkedTodos.isNotEmpty()) {
add(Section("Checked", checkedTodos))
}
}
}
private fun List<Section>.toListElements() = map { section ->
buildList {
section.header?.let {
add(ListElement.Header(it))
}
section.todosWithRoles.forEach { (todo, role) ->
add(ListElement.Item(todo, role))
}
}
}.flatten()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment