Using Layouts
A Layout
keeps track of a View and its children and automatically arranges the children as sizes change. This happens (by default) whenever View's size
changes, or one of its children has its bounds
change. The View class also protects
its layout
property by default so it can encapsulate special handling if needed, but sub-classes are free to expose it.
This examples shows the use of
HorizontalFlowLayout
, which wraps a View's children from left to right within its bounds.
import io.nacular.doodle.core.container
import io.nacular.doodle.layout.HorizontalFlowLayout
fun horizontalLayout() {
//sampleStart
val container = container {}
container.layout = HorizontalFlowLayout() // Container exposes its layout
//sampleEnd
}
Changes to a View's transform
will not trigger layout.
Custom Layouts
Doodle comes with several useful layouts, including one based on constraints. But you can easily create custom Layouts by implementing the Layout
interface or using the simpleLayout
utility function.
import io.nacular.doodle.core.Layout
import io.nacular.doodle.core.Layout.Companion.simpleLayout
import io.nacular.doodle.core.PositionableContainer
import io.nacular.doodle.geometry.Rectangle
fun example() {
//sampleStart
class CustomLayout: Layout {
override fun layout(container: PositionableContainer) {
container.children.filter { it.visible }.forEach { child ->
child.bounds = Rectangle(/*...*/)
}
}
}
// DSL for basic layout
simpleLayout { container ->
container.children.filter { it.visible }.forEach { child ->
child.bounds = Rectangle(/*...*/)
}
}
//sampleEnd
}
Layout
works with PositionableContainer
instead of View
directly because the latter does not expose its children by design.
When layout triggers
Layouts are generally triggered whenever their container's size changes or a child of their container has a bounds change. But there are cases when this default behavior does not work as well. A good example is a Layout
that uses a child's idealSize
in positioning. Such a Layout
won't be invoked when a child's idealSize
changes, and will be out of date in some cases. The following demo shows this.
- Demo
- Usage
import io.nacular.doodle.core.Layout.Companion.simpleLayout
import io.nacular.doodle.core.container
import io.nacular.doodle.docs.utils.BlueView
import io.nacular.doodle.drawing.Color.Companion.Lightgray
import io.nacular.doodle.drawing.lighter
import io.nacular.doodle.drawing.rect
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.utils.Resizer
fun unresponsiveLayout() {
//sampleStart
container {
repeat(2) {
this += BlueView().apply { size = Size(50) }
}
// This Layout does not override
// requiresLayout(
// child: Positionable,
// of : PositionableContainer,
// old : SizePreferences,
// new : SizePreferences
// ): Boolean
// Which means it defaults to ignoring changes to child SizePreferences
layout = simpleLayout { container ->
var x = 0.0
container.children.forEach {
it.x = x
x += (it.idealSize?.width ?: it.width) + 1
}
}
size = Size(200)
render = {
rect(bounds.atOrigin, Lightgray.lighter())
}
Resizer(this)
}
//sampleEnd
}
Moving the slider changes the ideal width of the blue boxes. But the container isn't updated because the Layout
used does not indicate (via requiresLayout
) it needs an updated when a View's SizePreferences
change.
You can see that it is out of date by resizing the container after moving the slider.
This is why Doodle offers a Layout
the chance to customize when they are invoked. In fact, Layouts are asked whether they want to respond to several potential triggers. These include size
changes in the container, bounds
and SizePreferences
changes for children. The latter happens whenever minimumSize
or idealSize
are updated for a child. This way, a Layout
can fine tune what triggers it.
The following shows how updating the Layout
so it replies to requiresLayout
for this scenario fixes the issue.
- Demo
- Usage
import io.nacular.doodle.core.Layout
import io.nacular.doodle.core.Positionable
import io.nacular.doodle.core.PositionableContainer
import io.nacular.doodle.core.View.SizePreferences
import io.nacular.doodle.core.container
import io.nacular.doodle.docs.utils.BlueView
import io.nacular.doodle.drawing.Color.Companion.Lightgray
import io.nacular.doodle.drawing.lighter
import io.nacular.doodle.drawing.rect
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.utils.Resizer
fun responsiveLayout(updateOnIdealChange: Boolean) {
//sampleStart
container {
repeat(2) {
this += BlueView().apply { size = Size(50) }
}
layout = object: Layout {
// Request layout whenever a child's idealSize changes
// (and the updateOnIdealChange switch is tuned on)
override fun requiresLayout(
child: Positionable,
of : PositionableContainer,
old : SizePreferences,
new : SizePreferences
) = updateOnIdealChange && old.idealSize != new.idealSize
// This Layout is very unusual (b/c it is contrived) in that it does not depend
// on the container's size. So it ignores these changes.
override fun requiresLayout(container: PositionableContainer, old: Size, new: Size) = false
override fun layout(container: PositionableContainer) {
var x = 0.0
container.children.forEach {
it.x = x
x += (it.idealSize?.width ?: it.width) + 1
}
}
}
size = Size(200)
render = {
rect(bounds.atOrigin, Lightgray.lighter())
}
Resizer(this)
}
//sampleEnd
}
Notice that this Layout
will actually ignore changes to the container's size
! Layouts are free to do that if the container's size
is irrelevant to the positioning of its children. This is very unlikely, but there might be cases where one dimension of size
, maybe width
or height
is irrelevant. In which case the Layout
can ignore updates if only that component changes.