/* * Copyright 2022 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the specific language governing * permissions and limitations under the License. */
/** * A [Modifier] that draws a border around elements that are recomposing. The border increases in * size and interpolates from red to green as more recompositions occur before a timeout. */ @Stable fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier)
// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations // Modifier.composed will still remember unique data per call site. privateval recomposeModifier = Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) { // The total number of compositions that have occurred. We're not using a State<> here be // able to read/write the value without invalidating (which would cause infinite // recomposition). val totalCompositions = remember { arrayOf(0L) } totalCompositions[0]++
// The value of totalCompositions at the last timeout. val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) }
// Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions // as the key is really just to cause the timer to restart every composition). LaunchedEffect(totalCompositions[0]) { delay(3000) totalCompositionsAtLastTimeout.value = totalCompositions[0] }
Modifier.drawWithCache { onDrawWithContent { // Draw actual content. drawContent()
// Below is to draw the highlight, if necessary. A lot of the logic is copied from // Modifier.border val numCompositionsSinceTimeout = totalCompositions[0] - totalCompositionsAtLastTimeout.value
val hasValidBorderParams = size.minDimension > 0f if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) { return@onDrawWithContent }
val (color, strokeWidthPx) = when (numCompositionsSinceTimeout) { // We need at least one composition to draw, so draw the smallest border // color in blue. 1L -> Color.Blue to 1f // 2 compositions is _probably_ okay. 2L -> Color.Green to 2.dp.toPx() // 3 or more compositions before timeout may indicate an issue. lerp the // color from yellow to red, and continually increase the border size. else -> { lerp( Color.Yellow.copy(alpha = 0.8f), Color.Red.copy(alpha = 0.5f), min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f) ) to numCompositionsSinceTimeout.toInt().dp.toPx() } }
val halfStroke = strokeWidthPx / 2 val topLeft = Offset(halfStroke, halfStroke) val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
val fillArea = (strokeWidthPx * 2) > size.minDimension val rectTopLeft = if (fillArea) Offset.Zero else topLeft val size = if (fillArea) size else borderSize val style = if (fillArea) Fill else Stroke(strokeWidthPx)