Skip to content

Commit

Permalink
Support layout and rendering at different pixel densities
Browse files Browse the repository at this point in the history
The layout algorithm in Rust remains unchanged and is done at the raw pixel level (matching Figma). When getting layout values from the layout manager to use in Android, layout values are multiplied by the current pixel density. There are a few scenarios where support for densities other than 1 needed additional changes:
- Code that referenced the pixel values specified in ViewStyle, such as margin, width or height, now multiply those values by the pixel density using the pointsAsDp(density) function. This has been fixed in a number of places including dials and gauges and masks, which previously had bugs.
- Since text size is calculated in Android and then sent back to the Rust layout system, the size is converted back into raw pixels by dividing by the pixel density first. When calculating text size given existing size data, convert the size data from raw pixels first by multiplying with the pixel density.
  • Loading branch information
rylin8 committed Sep 28, 2023
1 parent a51bb6c commit ecd6165
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 62 deletions.
2 changes: 1 addition & 1 deletion crates/layout/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ impl LayoutManager {
}

// Apply any customizations that have been saved for this node
fn apply_customizations(&mut self, layout_id: i32, style: &mut taffy::style::Style) {
fn apply_customizations(&self, layout_id: i32, style: &mut taffy::style::Style) {
let size = self.customizations.get_size(layout_id);
if let Some(size) = size {
style.min_size.width = taffy::prelude::Dimension::Points(size.width as f32);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,14 @@ internal fun ViewShape.computePaths(
stroke.map { p -> p.asPath(density, scaleX, scaleY) }
)
}
fun getRectSize(rectSize: Size?, style: ViewStyle): Size {
fun getRectSize(rectSize: Size?, style: ViewStyle, density: Float): Size {
val width =
rectSize?.width
?: if (style.width is Dimension.Points) (style.width as Dimension.Points).value
?: if (style.width is Dimension.Points) style.width.pointsAsDp(density).value
else frameSize.width
val height =
rectSize?.height
?: if (style.height is Dimension.Points) (style.height as Dimension.Points).value
?: if (style.height is Dimension.Points) style.height.pointsAsDp(density).value
else frameSize.height
return Size(width, height)
}
Expand All @@ -132,23 +132,23 @@ internal fun ViewShape.computePaths(
style,
listOf(0.0f, 0.0f, 0.0f, 0.0f),
density,
getRectSize(rectSize, style),
getRectSize(rectSize, style, density),
)
}
is ViewShape.RoundRect -> {
return computeRoundRectPathsFast(
style,
this.corner_radius,
density,
getRectSize(rectSize, style)
getRectSize(rectSize, style, density)
)
}
is ViewShape.VectorRect -> {
return computeRoundRectPathsFast(
style,
this.corner_radius,
density,
getRectSize(rectSize, style)
getRectSize(rectSize, style, density)
)
}
is ViewShape.Path -> {
Expand All @@ -171,7 +171,7 @@ internal fun ViewShape.computePaths(
}
else -> {
val path = Path()
val size = getRectSize(rectSize, style)
val size = getRectSize(rectSize, style, density)
path.addRect(Rect(0.0f, 0.0f, size.width, size.height))
Pair(listOf(path), listOf())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ internal fun DesignFrame(
return if (count > 0) count else 1
}

val layout = LayoutManager.getLayout(layoutId)
val layout = LayoutManager.getLayoutWithDensity(layoutId)
val gridSizeModifier = Modifier.layoutSizeToModifier(layout)

val density = LocalDensity.current.density
Expand Down Expand Up @@ -497,10 +497,10 @@ internal fun DesignFrame(
userScrollEnabled = layoutInfo.scrollingEnabled,
contentPadding =
PaddingValues(
layoutInfo.padding.start.pointsAsDp(),
layoutInfo.padding.top.pointsAsDp(),
layoutInfo.padding.end.pointsAsDp(),
layoutInfo.padding.bottom.pointsAsDp(),
layoutInfo.padding.start.pointsAsDp(density),
layoutInfo.padding.top.pointsAsDp(density),
layoutInfo.padding.end.pointsAsDp(density),
layoutInfo.padding.bottom.pointsAsDp(density),
),
) {
lazyItemContent()
Expand Down Expand Up @@ -564,7 +564,7 @@ internal fun DesignFrame(
if (parentLayout?.isWidgetChild == true) {
// For direct children of a widget, render the frame as a box with the calculated
// layout size, then compose the frame's children with our custom layout
val layout = LayoutManager.getLayout(layoutId)
val layout = LayoutManager.getLayoutWithDensity(layoutId)
Box(m.layoutSizeToModifier(layout)) {
DesignFrameLayout(modifier, name, layoutId, layoutState) { content() }
}
Expand Down Expand Up @@ -613,8 +613,8 @@ internal fun Modifier.frameRender(
paint.blendMode = BlendMode.DstIn
val offset =
Offset(
-style.margin.start.pointsAsDp().value,
-style.margin.top.pointsAsDp().value
-style.margin.start.pointsAsDp(density).value,
-style.margin.top.pointsAsDp(density).value
)
val parentSize = maskInfo?.parentSize?.value ?: size
drawContext.canvas.withSaveLayer(Rect(offset, parentSize), paint) { render() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ internal fun DesignOverlay(
// Similar to a root view, tell the layout manager to defer layout computations until all
// child views have been added to the overlay
LayoutManager.deferComputations()
Log.d(TAG,"Overlay start")
Log.d(TAG, "Overlay start")
onDispose {}
}
Box(boxModifier, contentAlignment = alignment) { content() }
DisposableEffect(Unit) {
// Similar to a root view, tell the layout manager to that child views have been added so
// that layout can be computed
LayoutManager.resumeComputations()
Log.d(TAG,"Overlay end")
Log.d(TAG, "Overlay end")
onDispose {}
}
}
41 changes: 30 additions & 11 deletions designcompose/src/main/java/com/android/designcompose/DesignText.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.isIdentity
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontLoader
import androidx.compose.ui.text.AnnotatedString
Expand Down Expand Up @@ -224,7 +225,12 @@ internal fun DesignText(
TextLayoutData(annotatedText, textStyle, LocalFontLoader.current, style.text_size)
val maxLines = if (style.line_count.isPresent) style.line_count.get().toInt() else Int.MAX_VALUE
val textMeasureData =
TextMeasureData(textLayoutData, density, maxLines, style.min_width.pointsAsDp().value)
TextMeasureData(
textLayoutData,
density,
maxLines,
style.min_width.pointsAsDp(density.density).value
)
val useMeasure = isAutoHeightFillWidth(style)

// Get the layout for this view that describes its size and position.
Expand Down Expand Up @@ -314,8 +320,8 @@ internal fun DesignText(
)
} else {
// Text needs to use a modifier that sets the size so that it wraps properly
val textModifier =
modifier.sizeToModifier(layout?.width() ?: 0, layout?.height() ?: 0)
val height = renderHeight ?: layout?.height() ?: 0
val textModifier = modifier.sizeToModifier(layout?.width() ?: 0, height)
BasicText(
annotatedText,
modifier = textModifier,
Expand All @@ -331,7 +337,16 @@ internal fun DesignText(
.wrapContentSize(align = Alignment.TopStart, unbounded = true)
.textTransform(style)
.layoutStyle(name, layoutId)
DesignTextLayout(layoutModifier, name, layout, layoutState, renderHeight, renderTop, content)
val layoutWithDensity = layout?.withDensity(density.density)
DesignTextLayout(
layoutModifier,
name,
layoutWithDensity,
layoutState,
renderHeight,
renderTop,
content
)
return true
}

Expand All @@ -345,6 +360,11 @@ fun measureTextBoundsFunc(
availableWidth: Float,
availableHeight: Float
): Pair<Float, Float> {
val density = LayoutManager.getDensity()
val width = width * density
val availableWidth = availableWidth * density
val availableHeight = availableHeight * density

val textMeasureData = LayoutManager.getTextMeasureData(layoutId)
if (textMeasureData == null) {
Log.d(TAG, "measureTextBoundsFunc() error: no textMeasureData for layoutId $layoutId")
Expand All @@ -367,7 +387,8 @@ fun measureTextBoundsFunc(
val outHeight =
if (availableHeight > 0f && rectBounds.height().toFloat() > availableHeight) availableHeight
else rectBounds.height().toFloat()
return Pair(rectBounds.width().toFloat(), outHeight)

return Pair(rectBounds.width().toFloat() / density, outHeight / density)
}

private class TextBounds(
Expand Down Expand Up @@ -397,18 +418,16 @@ private fun measureTextBounds(
// defined in Figma, but when the text is set to auto height then this gets set to the height
// calculated by boundsForWidth().
var rectBounds: android.graphics.Rect
var layoutHeight =
if (style.height is Dimension.Points) (style.height as Dimension.Points).value.roundToInt()
else 0
var layoutHeight: Int
when (val width = style.width) {
is Dimension.Points -> {
// Fixed width
textWidth = width.value.roundToInt()
textWidth = width.pointsAsDp(density.density).value.roundToInt()
when (style.height) {
is Dimension.Points -> {
// Fixed height. Get actual height so we can calculate vertical alignment
rectBounds = textLayoutData.boundsForWidth(Int.MAX_VALUE, 1, density).first
renderHeight = (style.height as Dimension.Points).value.roundToInt()
renderHeight = style.height.pointsAsDp(density.density).value.roundToInt()
layoutHeight = renderHeight
}
else -> {
Expand All @@ -433,7 +452,7 @@ private fun measureTextBounds(
rectBounds = textLayoutData.boundsForWidth(Int.MAX_VALUE, 1, density).first
textWidth = rectBounds.width()
renderHeight = rectBounds.height()
layoutHeight = textLayoutData.textBoxSize.height.roundToInt()
layoutHeight = (textLayoutData.textBoxSize.height * density.density).roundToInt()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
Expand Down Expand Up @@ -959,7 +960,7 @@ internal fun DesignDocInternal(
// Render debug node names, if turned on
Box(Modifier.fillMaxSize()) { DebugNodeManager.DrawNodeNames() }
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopEnd) {
DesignSwitcher(doc, docId, branchHash, switchDocId)
// DesignSwitcher(doc, docId, branchHash, switchDocId)
}
}
}
Expand All @@ -973,13 +974,20 @@ internal fun DesignDocInternal(
if (startFrame != null) {
LaunchedEffect(docId) { designComposeCallbacks?.docReadyCallback?.invoke(docId) }
CompositionLocalProvider(LocalDesignIsRootContext provides DesignIsRoot(false)) {
// Whenever the root view changes, call deferComputations() so that we defer layout calculation
// Whenever the root view changes, call deferComputations() so that we defer layout
// calculation
// until all views have been added
if (isRoot)
if (isRoot) {
DisposableEffect(startFrame) {
LayoutManager.deferComputations()
onDispose {}
}
val density = LocalDensity.current.density
DisposableEffect(density) {
LayoutManager.setDensity(density)
onDispose {}
}
}

DesignView(
modifier.semantics { sDocId = docId },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ private fun calculateParentOffsets(
val decomposed = style.transform.decompose(density)

// X Node position offset by the X translation value of the transform matrix
val nodeX = style.margin.start.pointsAsDp().value.toDouble() + decomposed.translateX
val nodeX = style.margin.start.pointsAsDp(density).value.toDouble() + decomposed.translateX
// Y Node position offset by the Y translation value of the transform matrix
val nodeY = style.margin.top.pointsAsDp().value.toDouble() + decomposed.translateY
val nodeY = style.margin.top.pointsAsDp(density).value.toDouble() + decomposed.translateY

// Radius of the circle encapsulating the node
val r = sqrt(nodeWidth * nodeWidth + nodeHeight * nodeHeight) / 2
Expand Down Expand Up @@ -106,8 +106,8 @@ private fun calculateRotationData(
(rotationData.start + meterValue / 100f * (rotationData.end - rotationData.start))
.coerceDiscrete(rotationData.discrete, rotationData.discreteValue)

val nodeWidth = style.width.pointsAsDp().value
val nodeHeight = style.height.pointsAsDp().value
val nodeWidth = style.width.pointsAsDp(density).value
val nodeHeight = style.height.pointsAsDp(density).value

// Calculate offsets from parent when the rotation is 0
val offsets =
Expand All @@ -134,8 +134,8 @@ private fun calculateRotationData(
// Translate back, with an additional offset from the parent
val translateBack = androidx.compose.ui.graphics.Matrix()
translateBack.translate(
moveX - style.margin.start.pointsAsDp().value + xOffsetParent.toFloat(),
moveY - style.margin.top.pointsAsDp().value + yOffsetParent.toFloat(),
moveX - style.margin.start.pointsAsDp(density).value + xOffsetParent.toFloat(),
moveY - style.margin.top.pointsAsDp(density).value + yOffsetParent.toFloat(),
0f
)
overrideTransform.timesAssign(translateBack)
Expand Down Expand Up @@ -171,7 +171,7 @@ private fun calculateProgressMarkerData(
// along the x axis
val moveX = lerp(markerData.startX, markerData.endX, discretizedMeterValue, density)
val overrideTransform = style.getTransform(density)
val leftOffset = style.margin.start.pointsAsDp().value
val leftOffset = style.margin.start.pointsAsDp(density).value
overrideTransform.setXTranslation(moveX - leftOffset)

return overrideTransform
Expand Down Expand Up @@ -253,7 +253,7 @@ internal fun ContentDrawScope.render(
calculateProgressBarData(
progressBarData,
meterValue,
style.height.pointsAsDp().value,
style.height.pointsAsDp(density).value,
density
)
}
Expand Down
23 changes: 20 additions & 3 deletions designcompose/src/main/java/com/android/designcompose/Layout.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ internal object LayoutManager {
private var textMeasures: HashMap<Int, TextMeasureData> = HashMap()
private var nextLayoutId: Int = 0
private var docLoaded: Boolean = false
private var density: Float = 1F

internal fun getNextLayoutId(): Int {
return ++nextLayoutId
Expand All @@ -100,6 +101,15 @@ internal object LayoutManager {
handleResponse(responseBytes)
}

internal fun setDensity(pixelDensity: Float) {
Log.i(TAG, "setDensity $pixelDensity")
density = pixelDensity
}

internal fun getDensity(): Float {
return density
}

internal fun subscribe(
layoutId: Int,
setLayoutState: (Int) -> Unit,
Expand Down Expand Up @@ -175,6 +185,11 @@ internal object LayoutManager {
return textMeasures[layoutId]
}

// Ask for the layout for the associated node via JNI
internal fun getLayoutWithDensity(layoutId: Int): Layout? {
return getLayout(layoutId)?.withDensity(density)
}

// Ask for the layout for the associated node via JNI
internal fun getLayout(layoutId: Int): Layout? {
val layoutBytes = Jni.jniGetLayout(layoutId)
Expand All @@ -188,7 +203,9 @@ internal object LayoutManager {
// Tell the Rust layout manager that a node size has changed. In the returned response, get all
// the nodes that have changed and notify subscribers of this change.
internal fun setNodeSize(layoutId: Int, width: Int, height: Int) {
val responseBytes = Jni.jniSetNodeSize(layoutId, width, height)
val adjustedWidth = (width.toFloat() / density).roundToInt()
val adjustedHeight = (height.toFloat() / density).roundToInt()
val responseBytes = Jni.jniSetNodeSize(layoutId, adjustedWidth, adjustedHeight)
handleResponse(responseBytes)
}

Expand Down Expand Up @@ -542,7 +559,7 @@ internal fun designMeasurePolicy(name: String, layoutId: Int) =
MeasurePolicy { measurables, constraints ->
val placeables = measurables.map { measurable -> measurable.measure(constraints) }

var myLayout = LayoutManager.getLayout(layoutId)
var myLayout = LayoutManager.getLayoutWithDensity(layoutId)
if (myLayout == null) {
Log.d(TAG, "designMeasurePolicy error: null layout $name layoutId $layoutId")
}
Expand All @@ -560,7 +577,7 @@ internal fun designMeasurePolicy(name: String, layoutId: Int) =
Log.d(TAG, "Place $name index $index: $myX, $myY}")
placeable.place(myX, myY)
} else {
val childLayout = LayoutManager.getLayout(layoutData.layoutId)
val childLayout = LayoutManager.getLayoutWithDensity(layoutData.layoutId)
if (childLayout == null) {
Log.d(
TAG,
Expand Down
Loading

0 comments on commit ecd6165

Please sign in to comment.