Repository navigation
Grid
The Grid examples assemble their setups with a very simple method: constrain the first hexagon to the top-start corner of the parent, then form a row by linking another hexagon to the first, then another to that second one, and so on until the end. A new row is started by linking to the first hexagon in the previous row, thus making it just like reading (in most languages) for the given layout direction: row by row, start to end, top to bottom.
Both versions also have the ConstraintLayout wrap its contents, and then
center it inside a plain container, like FrameLayout and Box, 'cause I don't
think it's possible to have ConstraintLayout center these children as a group,
since technically they're not chained together.
Both versions use the same algorithm to initialize and add their children, so we'll just take a look at View's here, and note the differences for Compose down below.
The Grids also demonstrate two more options when fitting the content to the container, in addition to the columns-to-width fit that the Shields do.
-
The vertical analog of that: fitting a certain number of rows to the available height.
-
Or, fitting the maximum number of rows and columns in the available space for a given hexagon size.
Again, these fittings are accomplished with helper functions located in
Calculations, with the example grid values being handled
and applied in ExampleData.kt.
In building the grid, the children's View IDs are calculated from their grid positions, giving us an easy way to figure out which one links to which.
for (row in 0..<rowCount) {
for (column in 0..<columnCount) {
val viewId = row * columnCount + column + 1 // View.id should be > 0
val params = ConstraintLayout.LayoutParams(
hexWidth.roundToInt(),
hexHeight.roundToInt()
)
when {
row == 0 && column == 0 -> {
params.startToStart = PARENT_ID
params.topToTop = PARENT_ID
}
column == 0 -> {
params.circleConstraint = viewId - columnCount
params.circleRadius = radius
params.circleAngle =
naturalRowStartAngle(isHorizontal, isLtr, row)
}
else -> {
params.circleConstraint = viewId - 1
params.circleRadius = radius
params.circleAngle =
naturalRowTailAngle(isHorizontal, isLtr, column)
}
}
val view = TextView(context).apply {
id = viewId
…
}
addView(view, params)
}
}Apart from which neighbor to link to, each hexagon also needs to know its
relative angle, and that is determined by two more functions from
Calculations: naturalRowStartAngle and
naturalRowTailAngle(), which are really just tiny lookup tables in the form of
nested whens.
Again, the Compose version is very similar to the View one, with adjustments for
the framework. Really, other than the basic differences you've seen already, the
only difference is that we track ConstrainedLayoutReferences instead of IDs,
so the position calculation is used to index into a list of those references,
rather than being directly associated with the Hexagons.
ConstraintLayout {
val refs = mutableListOf<ConstrainedLayoutReference>()
for (row in 0..<data.rowCount) {
for (column in 0..<data.columnCount) {
val index = row * data.columnCount + column
val ref = createRef().also { refs += it }
…
Hexagon(
ref = ref,
size = dpSize,
shape = shape,
borderStroke = BorderStroke(2.dp, color),
content = { Text(text = (index + 1).toString()) }
) {
when {
row == 0 && column == 0 -> {
start.linkTo(parent.start)
top.linkTo(parent.top)
}
column == 0 -> {
val angle = naturalRowStartAngle(
isHorizontal = isHorizontal,
isLtr = isLtr,
row = row
)
circular(
other = refs[index - data.columnCount],
angle = angle,
distance = radius
)
}
else -> {
val angle = naturalRowTailAngle(
isHorizontal = isHorizontal,
isLtr = isLtr,
column = column
)
circular(
other = refs[index - 1],
angle = angle,
distance = radius
)
}
}
}
}
}
}