Skip to content

Instantly share code, notes, and snippets.

@zach-klippenstein
Last active June 26, 2022 01:38
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zach-klippenstein/1f41043b8c0d4a5b84f6d3b2d6185a63 to your computer and use it in GitHub Desktop.
Save zach-klippenstein/1f41043b8c0d4a5b84f6d3b2d6185a63 to your computer and use it in GitHub Desktop.
Proof-of-concept of a composable that draws information about all its children on top of them.
@Composable fun App() {
DebugBounds {
Column(Modifier.background(Color.White).fillMaxSize()) {
BasicText("Some text")
Spacer(Modifier.size(10.dp))
BasicText("More text")
Spacer(Modifier.size(5.dp))
BasicText("Button", Modifier
.clickable { }
.background(Color.Blue, RoundedCornerShape(3.dp))
.padding(8.dp)
)
}
}
}
@Composable fun DebugBounds(content: @Composable () -> Unit) {
val recomposeScope = currentRecomposeScope
val boundsTracker = remember { BoundsTracker(recomposeScope) }
ObserveLayoutInfos(
onLayoutInfoInserted = boundsTracker::onLayoutInfoInserted,
modifier = Modifier.drawWithCache {
with(boundsTracker) {
draw()
}
},
content = content
)
}
private class BoundsTracker(private val recomposeScope: RecomposeScope) {
private val infos = mutableListOf<LayoutInfo>()
fun onLayoutInfoInserted(layoutInfo: LayoutInfo) {
infos += layoutInfo
recomposeScope.invalidate()
}
fun CacheDrawScope.draw(): DrawResult {
val textPaint = Paint().apply {
color = android.graphics.Color.RED
textSize = 24.sp.toPx()
}
return onDrawWithContent {
drawContent()
drawBounds(textPaint)
}
}
private fun DrawScope.drawBounds(textPaint: Paint) {
infos.forEach { info ->
if (info.isPlaced && info.isAttached) {
val bounds = info.coordinates.boundsInRoot()
drawRect(Color.Red, topLeft = bounds.topLeft, size = bounds.size, style = Stroke(1f))
drawContext.canvas.nativeCanvas.apply {
drawText(
"${bounds.width.roundToInt()}×${bounds.height.roundToInt()}",
bounds.left + 1f,
bounds.top - textPaint.fontMetrics.top - 1f,
textPaint
)
}
}
}
}
}
@OptIn(ExperimentalComposeApi::class, ComposeCompilerApi::class)
@Composable private fun ObserveLayoutInfos(
onLayoutInfoInserted: (LayoutInfo) -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
BoxWithConstraints(modifier) {
val composer = currentComposer
val context = rememberCompositionContext()
val subComposition = remember { ObservedComposition(composer.applier, context) }
subComposition.observingApplier.onLayoutInfoInserted = onLayoutInfoInserted
subComposition.composition.setContent {
Box(Modifier.sizeIn(maxWidth = maxWidth, maxHeight = maxHeight)) {
content()
}
}
}
}
@OptIn(ExperimentalComposeApi::class)
private class ObservedComposition<T>(
applier: Applier<T>,
parent: CompositionContext
) : RememberObserver {
val observingApplier = LayoutInfoObservingApplier(applier)
val composition = Composition(applier = observingApplier, parent = parent)
override fun onAbandoned() {
composition.dispose()
}
override fun onRemembered() {
// noop
}
override fun onForgotten() {
composition.dispose()
}
}
private class LayoutInfoObservingApplier<T>(
private val wrapped: Applier<T>
) : Applier<T> by wrapped {
var onLayoutInfoInserted: (LayoutInfo) -> Unit = {}
override fun insertBottomUp(
index: Int,
instance: T
) {
wrapped.insertBottomUp(index, instance)
if (instance is LayoutInfo) {
onLayoutInfoInserted(instance)
}
}
override fun insertTopDown(
index: Int,
instance: T
) {
wrapped.insertTopDown(index, instance)
if (instance is LayoutInfo) {
onLayoutInfoInserted(instance)
}
}
}
@zach-klippenstein
Copy link
Author

Screenshot_1614975517

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment