Skip to content

Commit 9942a9a

Browse files
committed
Merge branch 'main' into latest
2 parents 74aaf25 + 1a6cb36 commit 9942a9a

File tree

8 files changed

+680
-159
lines changed

8 files changed

+680
-159
lines changed

.github/workflows/apply_spotless.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
uses: actions/checkout@v3
3434
with:
3535
token: ${{ secrets.PAT || github.token }}
36+
fetch-depth: 0
3637

3738
- name: set up Java 17
3839
uses: actions/setup-java@v2

compose/recomposehighlighter/src/main/java/com/example/android/compose/recomposehighlighter/RecomposeHighlighter.kt

Lines changed: 109 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -16,100 +16,139 @@
1616

1717
package com.example.android.compose.recomposehighlighter
1818

19-
import androidx.compose.runtime.LaunchedEffect
2019
import androidx.compose.runtime.Stable
21-
import androidx.compose.runtime.mutableStateOf
22-
import androidx.compose.runtime.remember
2320
import androidx.compose.ui.Modifier
24-
import androidx.compose.ui.composed
25-
import androidx.compose.ui.draw.drawWithCache
2621
import androidx.compose.ui.geometry.Offset
2722
import androidx.compose.ui.geometry.Size
2823
import androidx.compose.ui.graphics.Color
2924
import androidx.compose.ui.graphics.SolidColor
25+
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
3026
import androidx.compose.ui.graphics.drawscope.Fill
3127
import androidx.compose.ui.graphics.drawscope.Stroke
3228
import androidx.compose.ui.graphics.lerp
29+
import androidx.compose.ui.node.DrawModifierNode
30+
import androidx.compose.ui.node.ModifierNodeElement
31+
import androidx.compose.ui.node.invalidateDraw
32+
import androidx.compose.ui.platform.InspectorInfo
3333
import androidx.compose.ui.platform.debugInspectorInfo
3434
import androidx.compose.ui.unit.dp
35+
import java.util.Objects
3536
import kotlin.math.min
37+
import kotlinx.coroutines.Job
3638
import kotlinx.coroutines.delay
39+
import kotlinx.coroutines.launch
3740

3841
/**
3942
* A [Modifier] that draws a border around elements that are recomposing. The border increases in
4043
* size and interpolates from red to green as more recompositions occur before a timeout.
4144
*/
4245
@Stable
43-
fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier)
44-
45-
// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations
46-
// Modifier.composed will still remember unique data per call site.
47-
private val recomposeModifier =
48-
Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) {
49-
// The total number of compositions that have occurred. We're not using a State<> here be
50-
// able to read/write the value without invalidating (which would cause infinite
51-
// recomposition).
52-
val totalCompositions = remember { arrayOf(0L) }
53-
totalCompositions[0]++
54-
55-
// The value of totalCompositions at the last timeout.
56-
val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) }
57-
58-
// Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions
59-
// as the key is really just to cause the timer to restart every composition).
60-
LaunchedEffect(totalCompositions[0]) {
46+
fun Modifier.recomposeHighlighter(): Modifier = this.then(RecomposeHighlighterElement())
47+
48+
private class RecomposeHighlighterElement : ModifierNodeElement<RecomposeHighlighterModifier>() {
49+
50+
override fun InspectorInfo.inspectableProperties() {
51+
debugInspectorInfo { name = "recomposeHighlighter" }
52+
}
53+
54+
override fun create(): RecomposeHighlighterModifier = RecomposeHighlighterModifier()
55+
56+
override fun update(node: RecomposeHighlighterModifier) {
57+
node.incrementCompositions()
58+
}
59+
60+
// It's never equal, so that every recomposition triggers the update function.
61+
override fun equals(other: Any?): Boolean = false
62+
63+
override fun hashCode(): Int = Objects.hash(this)
64+
}
65+
66+
private class RecomposeHighlighterModifier : Modifier.Node(), DrawModifierNode {
67+
68+
private var timerJob: Job? = null
69+
70+
/**
71+
* The total number of compositions that have occurred.
72+
*/
73+
private var totalCompositions: Long = 0
74+
set(value) {
75+
if (field == value) return
76+
if (value > 0) restartTimer()
77+
field = value
78+
invalidateDraw()
79+
}
80+
81+
fun incrementCompositions() {
82+
totalCompositions++
83+
}
84+
85+
override fun onAttach() {
86+
super.onAttach()
87+
restartTimer()
88+
}
89+
90+
override val shouldAutoInvalidate: Boolean = false
91+
92+
override fun onDetach() {
93+
timerJob?.cancel()
94+
}
95+
96+
/**
97+
* Start the timeout, and reset everytime there's a recomposition.
98+
*/
99+
private fun restartTimer() {
100+
if (!isAttached) return
101+
102+
timerJob?.cancel()
103+
timerJob = coroutineScope.launch {
61104
delay(3000)
62-
totalCompositionsAtLastTimeout.value = totalCompositions[0]
105+
totalCompositions = 0
63106
}
107+
}
64108

65-
Modifier.drawWithCache {
66-
onDrawWithContent {
67-
// Draw actual content.
68-
drawContent()
109+
override fun ContentDrawScope.draw() {
110+
// Draw actual content.
111+
drawContent()
69112

70-
// Below is to draw the highlight, if necessary. A lot of the logic is copied from
71-
// Modifier.border
72-
val numCompositionsSinceTimeout =
73-
totalCompositions[0] - totalCompositionsAtLastTimeout.value
113+
// Below is to draw the highlight, if necessary. A lot of the logic is copied from Modifier.border
74114

75-
val hasValidBorderParams = size.minDimension > 0f
76-
if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) {
77-
return@onDrawWithContent
78-
}
115+
val hasValidBorderParams = size.minDimension > 0f
116+
if (!hasValidBorderParams || totalCompositions <= 0) {
117+
return
118+
}
79119

80-
val (color, strokeWidthPx) =
81-
when (numCompositionsSinceTimeout) {
82-
// We need at least one composition to draw, so draw the smallest border
83-
// color in blue.
84-
1L -> Color.Blue to 1f
85-
// 2 compositions is _probably_ okay.
86-
2L -> Color.Green to 2.dp.toPx()
87-
// 3 or more compositions before timeout may indicate an issue. lerp the
88-
// color from yellow to red, and continually increase the border size.
89-
else -> {
90-
lerp(
91-
Color.Yellow.copy(alpha = 0.8f),
92-
Color.Red.copy(alpha = 0.5f),
93-
min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f)
94-
) to numCompositionsSinceTimeout.toInt().dp.toPx()
95-
}
96-
}
97-
98-
val halfStroke = strokeWidthPx / 2
99-
val topLeft = Offset(halfStroke, halfStroke)
100-
val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
101-
102-
val fillArea = (strokeWidthPx * 2) > size.minDimension
103-
val rectTopLeft = if (fillArea) Offset.Zero else topLeft
104-
val size = if (fillArea) size else borderSize
105-
val style = if (fillArea) Fill else Stroke(strokeWidthPx)
106-
107-
drawRect(
108-
brush = SolidColor(color),
109-
topLeft = rectTopLeft,
110-
size = size,
111-
style = style
112-
)
120+
val (color, strokeWidthPx) =
121+
when (totalCompositions) {
122+
// We need at least one composition to draw, so draw the smallest border
123+
// color in blue.
124+
1L -> Color.Blue to 1f
125+
// 2 compositions is _probably_ okay.
126+
2L -> Color.Green to 2.dp.toPx()
127+
// 3 or more compositions before timeout may indicate an issue. lerp the
128+
// color from yellow to red, and continually increase the border size.
129+
else -> {
130+
lerp(
131+
Color.Yellow.copy(alpha = 0.8f),
132+
Color.Red.copy(alpha = 0.5f),
133+
min(1f, (totalCompositions - 1).toFloat() / 100f),
134+
) to totalCompositions.toInt().dp.toPx()
135+
}
113136
}
114-
}
137+
138+
val halfStroke = strokeWidthPx / 2
139+
val topLeft = Offset(halfStroke, halfStroke)
140+
val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
141+
142+
val fillArea = (strokeWidthPx * 2) > size.minDimension
143+
val rectTopLeft = if (fillArea) Offset.Zero else topLeft
144+
val size = if (fillArea) size else borderSize
145+
val style = if (fillArea) Fill else Stroke(strokeWidthPx)
146+
147+
drawRect(
148+
brush = SolidColor(color),
149+
topLeft = rectTopLeft,
150+
size = size,
151+
style = style,
152+
)
115153
}
154+
}

compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.example.compose.snippets.components.CardExamples
3636
import com.example.compose.snippets.components.CheckboxExamples
3737
import com.example.compose.snippets.components.ChipExamples
3838
import com.example.compose.snippets.components.ComponentsScreen
39+
import com.example.compose.snippets.components.DatePickerExamples
3940
import com.example.compose.snippets.components.DialogExamples
4041
import com.example.compose.snippets.components.DividerExamples
4142
import com.example.compose.snippets.components.FloatingActionButtonExamples
@@ -107,6 +108,7 @@ class SnippetsActivity : ComponentActivity() {
107108
TopComponentsDestination.BadgeExamples -> BadgeExamples()
108109
TopComponentsDestination.PartialBottomSheet -> PartialBottomSheet()
109110
TopComponentsDestination.TimePickerExamples -> TimePickerExamples()
111+
TopComponentsDestination.DatePickerExamples -> DatePickerExamples()
110112
}
111113
}
112114
}

compose/snippets/src/main/java/com/example/compose/snippets/components/AppBar.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ fun CenterAlignedTopAppBarExample() {
204204

205205
topBar = {
206206
CenterAlignedTopAppBar(
207-
colors = TopAppBarDefaults.topAppBarColors(
207+
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
208208
containerColor = MaterialTheme.colorScheme.primaryContainer,
209209
titleContentColor = MaterialTheme.colorScheme.primary,
210210
),

0 commit comments

Comments
 (0)