Created
November 15, 2021 13:23
-
-
Save hyoban/66514403538e1a6b22ad8469094b910e to your computer and use it in GitHub Desktop.
jetpack custom layout example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Composable | |
fun MyColumn( | |
modifier: Modifier = Modifier, | |
content: @Composable () -> Unit, | |
) { | |
Layout( | |
// apply modifier to layout | |
modifier = modifier, | |
// slot for any child composables | |
content = content, | |
measurePolicy = { | |
measurables: List<Measurable>, | |
constrains: Constraints, | |
-> | |
// how the layout measure and place items | |
// implement this functional interface to implement custom layout behavior | |
// constrains object tell the layout how large or small it can be | |
// modeling minimum and maximum width and height | |
// a list of Measurable | |
// representation of child elements passed in | |
// expose functions for measuring items | |
// 1. measure any children | |
val placeables = measurables.map { | |
// accept size constrains | |
it.measure(constrains) | |
} | |
// produce a list of placeables (measured children, have a size) | |
// avoid to place an element that hasn't been measured | |
// in view world, it you decide when you call onMeasure ana onLayout | |
// we need to settle bugs and difference in behavior | |
// 2. calculate how big our layout should be | |
val height = placeables.sumOf { it.height } | |
val width = placeables.maxOf { it.width } | |
// report our size by calling layout method | |
layout(width, height) { | |
// placement block | |
// 3. place each items | |
var y = 0 | |
placeables.forEach { | |
// auto-mirror coordinates horizontally in rtl locales | |
it.placeRelative(x = 0, y = y) | |
y += it.height | |
} | |
} | |
} | |
) | |
} | |
@Composable | |
fun VerticalGrid( | |
modifier: Modifier = Modifier, | |
columns: Int = 2, | |
content: @Composable () -> Unit, | |
) { | |
Layout( | |
content = content, | |
modifier = modifier | |
) { measurables, constraints -> | |
val itemWidth = constraints.maxWidth / columns | |
val itemConstraints = constraints.copy( | |
minWidth = itemWidth, | |
maxWidth = itemWidth | |
) | |
// create different constraints to measure children | |
// no negotiation between the parent and child | |
// parent passes a range of allowable sizes, expressed as constraints | |
// child choose its size from this | |
// parent must accept and handle it | |
// good properties | |
// measure entire Ui tree in single pass, forbid multiple measurement cycles | |
// ( could lead to quadratic number of measurements on leaf views) | |
// measure an item twice throws an exception | |
// stronger performance guarantees make it open to new possibilities (such as animating layout) | |
val placeables = measurables.map { it.measure(itemConstraints) } | |
layout( | |
) | |
} | |
} | |
@Composable | |
fun BottomNavItem( | |
icon: @Composable BoxScope.() -> Unit, | |
text: @Composable BoxScope.() -> Unit, | |
@FloatRange(from = 0.0, to = 1.0) animationProgress: Float, | |
) { | |
Layout( | |
content = { | |
Box( | |
modifier = Modifier.layoutId("icon"), | |
content = icon | |
) | |
Box( | |
modifier = Modifier.layoutId("text"), | |
content = text | |
) | |
} | |
) { measurables, constraints -> | |
// identify the measurables | |
// more robust than relying on their ordering | |
val iconPlaceable = measurables.first { it.layoutId == "icon" }.measure(constraints) | |
val textPlaceable = measurables.first { it.layoutId == "text" }.measure(constraints) | |
placeTextAndIcon( | |
textPlaceable, | |
iconPlaceable, | |
constraints.maxWidth, | |
constraints.maxHeight, | |
animationProgress | |
) | |
} | |
} | |
// layouts in compose are so performant we can animate measurement | |
// or placement or drive them with a gesture | |
// it's hard to achieve in view system, animating layout was discouraged due to performance concerns | |
fun MeasureScope.placeTextAndIcon( | |
textPlaceable: Placeable, | |
iconPlaceable: Placeable, | |
width: Int, | |
height: Int, | |
@FloatRange(from = 0.0, to = 1.0) animationProgress: Float, | |
): MeasureResult { | |
val iconY = (height - iconPlaceable.height) / 2 | |
val textY = (height - textPlaceable.height) / 2 | |
val textWidth = textPlaceable.width * animationProgress | |
val iconX = (width - textWidth - iconPlaceable.width) / 2 | |
val textX = iconX + iconPlaceable.width | |
return layout(width, height) { | |
iconPlaceable.placeRelative(iconX.toInt(), iconY) | |
if (animationProgress != 0f) { | |
textPlaceable.placeRelative(textX.toInt(), textY) | |
} | |
} | |
} | |
// when to custom | |
// 1. hard to achieve with standard layouts | |
// 2. precise control of measurement/placement | |
// 3. layout animation | |
// 4. performance | |
// Layout Modifier | |
// measure method, identical to Layout composable | |
// only acts on a single measurable, because it's applied to a single item | |
// Modifier.layout | |
// modifier in layout | |
// Layout receive a single modifier parameter, this models a chain of modifiers are applied in order | |
@Composable | |
fun CenteredBox() { | |
Box( | |
Modifier | |
// 0-200, 0-300 | |
// outside constrains passed into here | |
// fillMaxSize create a new set of constrains | |
// set the min and max width and height to be equal to the maximum incoming width and height | |
// 200, 300 | |
// most layout wrap its content | |
// make measurement take up the entire space in order to center the box | |
.fillMaxSize() | |
// constrains passed down | |
// relax the incoming constraints, letting the content measure at its desired size | |
// 0-200, 0-300 | |
// here not to size it, but to center it | |
// to center it, use a layout modifier affects placement | |
// this measure at its desired size and use align parameter to place it | |
.wrapContentSize() | |
// constrains passed down | |
// create exact size constrains to measure the item with | |
// 50, 50 | |
.size(50.dp) | |
.background(Color.Yellow) | |
// passed to the box layout | |
// measure and return its resolved size by 50, 50 back up the modifier chain | |
// size modifier resolves its size and create placement instructions | |
// wrapContentSize modifier create placement instructions to center it | |
// this modifier knows its size is 200x300, and next element is 50x50 | |
// fillMaxSize modifier resolves its size and create placement instructions | |
// constrains are passed down, which subsequent elements use to measure themselves | |
// resolved sizes come back up and placement instructions are create d | |
) | |
} | |
// advanced feature | |
// intrinsic measurement | |
// we can't always do layouts in one pass | |
// sometimes need to know child size before finalizing the constraints | |
// parentData modifier | |
// layout offer some behavior that require some information from the child | |
// eg. boxScope align | |
// in custom layout | |
// val parentData = measurable.parentData as? MyParentData | |
// Alignment Lines align item by something else like textBaseline | |
// eg. alignByBaseline() or alignBy {} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment