Skip to content

Instantly share code, notes, and snippets.

@hyoban
Created November 15, 2021 13:23
Show Gist options
  • Save hyoban/66514403538e1a6b22ad8469094b910e to your computer and use it in GitHub Desktop.
Save hyoban/66514403538e1a6b22ad8469094b910e to your computer and use it in GitHub Desktop.
jetpack custom layout example
@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