Skip to content

Commit

Permalink
Update UIView Box Implementation (#1577)
Browse files Browse the repository at this point in the history
- Replaces Button with Rectangle in the Sandbox screen
- Fix for certain combinations of Wrap + Stretch not rendering correctly
- Introduce handling for explicit width and height modifiers
  • Loading branch information
underscoretang authored Oct 12, 2023
1 parent a3b529b commit 9cf3c0c
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ package app.cash.redwood.layout.uiview
import app.cash.redwood.Modifier
import app.cash.redwood.layout.api.Constraint
import app.cash.redwood.layout.api.CrossAxisAlignment
import app.cash.redwood.layout.modifier.Height
import app.cash.redwood.layout.modifier.HorizontalAlignment
import app.cash.redwood.layout.modifier.VerticalAlignment
import app.cash.redwood.layout.modifier.Width
import app.cash.redwood.layout.widget.Box
import app.cash.redwood.ui.Margin
import app.cash.redwood.ui.toPlatformDp
import app.cash.redwood.widget.UIViewChildren
import kotlin.math.max
import kotlinx.cinterop.CValue
import kotlinx.cinterop.convert
import kotlinx.cinterop.readValue
Expand Down Expand Up @@ -89,12 +93,14 @@ internal class UIViewBox : Box<UIView> {
children.widgets.forEach {
val view = it.value
view.sizeToFit()
var childWidth: CGFloat = view.frame.useContents { this.size.width }
var childHeight: CGFloat = view.frame.useContents { this.size.height }

// Check for modifier overrides in the children, otherwise default to the Box's alignment values
// Check for modifier overrides in the children, otherwise default to the Box's alignment values.
var itemHorizontalAlignment = horizontalAlignment
var itemVerticalAlignment = verticalAlignment

var requestedWidth: CGFloat? = null
var requestedHeight: CGFloat? = null

it.modifier.forEach { childModifier ->
when (childModifier) {
is HorizontalAlignment -> {
Expand All @@ -103,10 +109,20 @@ internal class UIViewBox : Box<UIView> {
is VerticalAlignment -> {
itemVerticalAlignment = childModifier.alignment
}
is Width -> {
requestedWidth = childModifier.width.toPlatformDp()
}
is Height -> {
requestedHeight = childModifier.height.toPlatformDp()
}
}
}

// Compute origin and stretch if needed
// Use requested modifiers, otherwise use the size established from sizeToFit().
var childWidth: CGFloat = requestedWidth ?: view.frame.useContents { this.size.width }
var childHeight: CGFloat = requestedHeight ?: view.frame.useContents { this.size.height }

// Compute origin and stretch if needed.
var x: CGFloat = 0.0
var y: CGFloat = 0.0
when (itemHorizontalAlignment) {
Expand All @@ -128,58 +144,76 @@ internal class UIViewBox : Box<UIView> {
CrossAxisAlignment.End -> y = frame.useContents { this.size.height } - childHeight
}

// Position the view
// Position the view.
view.setFrame(CGRectMake(x, y, childWidth, childHeight))
}
}

override fun sizeThatFits(size: CValue<CGSize>): CValue<CGSize> {
var width: CGFloat = 0.0
var height: CGFloat = 0.0
var maxItemWidth: CGFloat = 0.0
var maxItemHeight: CGFloat = 0.0

var maxRequestedWidth: CGFloat = 0.0
var maxRequestedHeight: CGFloat = 0.0

// Calculate the size based on Constraint Values
// Get the largest sizes based on explicit widget modifiers.
children.widgets.forEach {
it.modifier.forEach { childModifier ->
when (childModifier) {
is Width -> {
if (childModifier.width.value > maxRequestedWidth) {
maxRequestedWidth = childModifier.width.value
}
}
is Height -> {
if (childModifier.height.value > maxRequestedHeight) {
maxRequestedHeight = childModifier.height.value
}
}
}
}
}

// Calculate the size based on Constraint values.
when (widthConstraint) {
Constraint.Fill -> {
when (heightConstraint) {
Constraint.Fill -> {
width = size.useContents { this.width }
height = size.useContents { this.height }
Constraint.Fill -> { // Fill Fill
maxItemWidth = size.useContents { this.width }
maxItemHeight = size.useContents { this.height }
}
Constraint.Wrap -> {
height = size.useContents { this.height }

width = typedSubviews
.map { it.sizeThatFits(size).useContents { this.width } }
Constraint.Wrap -> { // Fill Wrap
maxItemWidth = size.useContents { this.width }
maxItemHeight = typedSubviews
.map { it.sizeThatFits(size).useContents { this.height } }
.max()
}
}
}
Constraint.Wrap -> {
when (heightConstraint) {
Constraint.Fill -> {
width = size.useContents { this.width }

// calculate the height of the biggest item
height = typedSubviews
.map { it.sizeThatFits(size).useContents { this.height } }
Constraint.Fill -> { // Wrap Fill
maxItemWidth = typedSubviews
.map { it.sizeThatFits(size).useContents { this.width } }
.max()
maxItemHeight = size.useContents { this.height }
}
Constraint.Wrap -> {
Constraint.Wrap -> { // Wrap Wrap
val unconstrainedSizes = typedSubviews
.map { it.sizeThatFits(size) }

width = unconstrainedSizes
maxItemWidth = unconstrainedSizes
.map { it.useContents { this.width } }
.max()

height = unconstrainedSizes
maxItemHeight = unconstrainedSizes
.map { it.useContents { this.height } }
.max()
}
}
}
}
return CGSizeMake(width, height)
return CGSizeMake(max(maxRequestedWidth, maxItemWidth), max(maxRequestedHeight, maxItemHeight))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,54 +18,78 @@ package com.example.redwood.testing.presenter
import androidx.compose.runtime.Composable
import app.cash.redwood.Modifier
import app.cash.redwood.layout.api.Constraint
import app.cash.redwood.layout.api.Constraint.Companion.Fill
import app.cash.redwood.layout.api.Constraint.Companion.Wrap
import app.cash.redwood.layout.api.CrossAxisAlignment
import app.cash.redwood.layout.api.CrossAxisAlignment.Companion.Center
import app.cash.redwood.layout.api.CrossAxisAlignment.Companion.End
import app.cash.redwood.layout.api.CrossAxisAlignment.Companion.Start
import app.cash.redwood.layout.api.CrossAxisAlignment.Companion.Stretch
import app.cash.redwood.layout.api.MainAxisAlignment
import app.cash.redwood.layout.api.MainAxisAlignment.Companion.SpaceBetween
import app.cash.redwood.layout.api.Overflow
import app.cash.redwood.layout.compose.Box
import app.cash.redwood.layout.compose.Column
import app.cash.redwood.layout.compose.ColumnScope
import app.cash.redwood.layout.compose.Row
import app.cash.redwood.layout.compose.Spacer
import app.cash.redwood.ui.Margin
import app.cash.redwood.ui.dp
import com.example.redwood.testing.compose.Button
import com.example.redwood.testing.compose.Rectangle
import com.example.redwood.testing.compose.Text

private val accentColor = 0xFFDDDDDDu
private val rowColor = 0xFFDDDDDDu
private val boxColor = 0xFFFFFF66u
private val backColor = 0x88FF0000u
private val middleColor = 0x8800FF00u
private val frontColor = 0x880000FFu

private val rowHeight = 80.dp

@Composable
fun BoxSandbox() {
Column(
width = Constraint.Fill,
height = Constraint.Fill,
width = Fill,
height = Fill,
overflow = Overflow.Scroll,
horizontalAlignment = CrossAxisAlignment.Stretch,
horizontalAlignment = Stretch,
verticalAlignment = MainAxisAlignment.Start,
) {
val crossAxisAlignments = listOf<CrossAxisAlignment>(
CrossAxisAlignment.Start,
CrossAxisAlignment.Center,
CrossAxisAlignment.Stretch,
CrossAxisAlignment.End,
val crossAxisAlignments = listOf(
Start,
Center,
Stretch,
End,
)

val constraints = listOf<Constraint>(
Constraint.Fill,
Constraint.Wrap,
val constraints = listOf(
Fill,
Wrap,
)

Legend()

// Uncomment to debug a specific permutation.
// BoxRow(
// width = Wrap,
// height = Wrap,
// horizontalAlignment = Start,
// verticalAlignment = Stretch,
// modifier = Modifier.height(140.dp),
// )

// Iterate over all permutations
constraints.forEach {
val widthConstraint = it
constraints.forEach {
val heightConstraint = it
crossAxisAlignments.forEach {
val horizontalAlignment = it
crossAxisAlignments.forEach {
val verticalAlignment = it
Text("$widthConstraint $heightConstraint $horizontalAlignment $verticalAlignment")
constraints.forEach { widthConstraint ->
constraints.forEach { heightConstraint ->
crossAxisAlignments.forEach { horizontalAlignment ->
crossAxisAlignments.forEach { verticalAlignment ->
BoxRow(
width = widthConstraint,
height = heightConstraint,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
modifier = Modifier.height(120.dp),
modifier = Modifier.height(rowHeight),
)
}
}
Expand All @@ -75,33 +99,123 @@ fun BoxSandbox() {
}

@Composable
private fun BoxRow(
private fun ColumnScope.Legend() {
Text(
"Legend",
modifier = Modifier.horizontalAlignment(Center).height(40.dp),
)
Box(
width = Fill,
height = Wrap,
horizontalAlignment = Stretch,
verticalAlignment = Start,
modifier = Modifier.height(80.dp).margin(Margin(horizontal = 20.dp)),
) {
Rectangle(
backgroundColor = accentColor,
cornerRadius = 12f,
modifier = Modifier.horizontalAlignment(Stretch).verticalAlignment(Stretch),
)
Column(
width = Fill,
horizontalAlignment = Center,
) {
Text(
"Constraints [x y] | Alignments [x y]",
modifier = Modifier.horizontalAlignment(Center).height(40.dp),
)
Row(
horizontalAlignment = SpaceBetween,
verticalAlignment = Center,
) {
Rectangle(
backgroundColor = boxColor,
cornerRadius = 4f,
modifier = Modifier.width(20.dp).height(20.dp),
)
Text(" Box")
Spacer(width = 12.dp)
Rectangle(
backgroundColor = backColor,
cornerRadius = 4f,
modifier = Modifier.width(20.dp).height(20.dp),
)
Text(" Back")
Spacer(width = 12.dp)
Rectangle(
backgroundColor = middleColor,
cornerRadius = 4f,
modifier = Modifier.width(20.dp).height(20.dp),
)
Text(" Middle")
Spacer(width = 12.dp)
Rectangle(
backgroundColor = frontColor,
cornerRadius = 4f,
modifier = Modifier.width(20.dp).height(20.dp),
)
Text(" Front")
}
}
}
}

@Composable
private fun ColumnScope.BoxRow(
width: Constraint,
height: Constraint,
horizontalAlignment: CrossAxisAlignment,
verticalAlignment: CrossAxisAlignment,
modifier: Modifier = Modifier,
) {
// Divider
Rectangle(accentColor, modifier = Modifier.height(1.dp).horizontalAlignment(Stretch).margin(Margin(top = 20.dp)))
Text(
"$width $height | $horizontalAlignment $verticalAlignment",
modifier = Modifier.horizontalAlignment(Center).height(40.dp),
)
Column(
width = Constraint.Fill,
height = Constraint.Fill,
horizontalAlignment = CrossAxisAlignment.Start,
width = Fill,
height = Fill,
horizontalAlignment = Start,
margin = Margin(horizontal = 20.dp, vertical = 0.dp),
modifier = modifier,
) {
Row(
width = Constraint.Fill,
height = Constraint.Fill,
Box(
width = Fill,
height = Fill,
horizontalAlignment = Stretch,
verticalAlignment = Stretch,
) {
Box(
width = width,
height = height,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
Rectangle(backgroundColor = rowColor)
Row(
width = Fill,
height = Fill,
) {
Button("BACK - BACK - BACK\nBACK - BACK - BACK\nBACK - BACK - BACK", onClick = null)
Button("MIDDLE - MIDDLE\nMIDDLE - MIDDLE", onClick = null)
Button("FRONT", onClick = null)
// This is the box we're measuring.
Box(
width = width,
height = height,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
// TODO: Add backgroundColor = boxColor once the modifiers are available.
) {
Rectangle(
backgroundColor = backColor,
cornerRadius = 12f,
modifier = Modifier.width((rowHeight.value * 1.4).dp).height((rowHeight.value * 0.8).dp),
)
Rectangle(
backgroundColor = middleColor,
cornerRadius = 12f,
modifier = Modifier.width((rowHeight.value * 1.2).dp).height((rowHeight.value * 0.6).dp),
)
Rectangle(
backgroundColor = frontColor,
cornerRadius = 12f,
modifier = Modifier.width((rowHeight.value).dp).height((rowHeight.value * 0.4).dp),
)
}
}
}
}
Expand Down

0 comments on commit 9cf3c0c

Please sign in to comment.