Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit 539f772

Browse files
Merge pull request #12 from zach-klippenstein/zachklipp/inspector
Introduce inspection mode for peering into the past.
2 parents b27ae49 + b437505 commit 539f772

6 files changed

Lines changed: 407 additions & 38 deletions

File tree

.images/inspector.gif

1.19 MB
Loading

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ You can use the `BackstackViewerApp` composable in the `backstack-viewer` artifa
9797
custom transitions interactively. This composable is used by the sample app, and in the screenshots
9898
below.
9999

100+
## Inspecting the backstack
101+
102+
The `Backstack` composable takes an optional `InspectionParams` parameter. When not null, the entire
103+
backstack will be rendered as a translucent 3D stack. The top-most screen in the stack will still
104+
be rendered in its regular position, but with a very low opacity, and will still be interactive. The
105+
`BackstackInspectorParams` controls how the stack is rendered, including rotation, scaling,
106+
opacity, etc.
107+
108+
You can wrap your `Backstack` with the `InspectionGestureDetector` composable to automatically
109+
control the inspector mode using touch gestures.
110+
111+
![Backstack inspector](.images/inspector.gif)
112+
100113
## Samples
101114

102115
There is a sample app in the `sample` module that demonstrates various transition animations and

backstack-viewer/src/main/java/com/zachklipp/compose/backstack/viewer/BackstackViewerApp.kt

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.zachklipp.compose.backstack.Backstack
1818
import com.zachklipp.compose.backstack.BackstackTransition
1919
import com.zachklipp.compose.backstack.BackstackTransition.Crossfade
2020
import com.zachklipp.compose.backstack.BackstackTransition.Slide
21+
import com.zachklipp.compose.backstack.InspectionGestureDetector
2122

2223
private val DEFAULT_BACKSTACKS = listOf(
2324
listOf("one"),
@@ -30,19 +31,20 @@ private val BUILTIN_BACKSTACK_TRANSITIONS = listOf(
3031
"Crossfade" to Crossfade
3132
)
3233

33-
@Preview
34-
@Composable
35-
private fun BackstackViewerAppPreview() {
36-
BackstackViewerApp()
37-
}
34+
//@Preview
35+
//@Composable
36+
//private fun BackstackViewerAppPreview() {
37+
// BackstackViewerApp()
38+
//}
3839

3940
@Model
4041
private class AppModel(
4142
var namedTransitions: List<Pair<String, BackstackTransition>>,
4243
var backstacks: List<Pair<String, List<String>>>,
4344
var selectedTransition: Pair<String, BackstackTransition> = namedTransitions.first(),
4445
var selectedBackstack: Pair<String, List<String>> = backstacks.first(),
45-
var slowAnimations: Boolean = false
46+
var slowAnimations: Boolean = false,
47+
var inspectionEnabled: Boolean = false
4648
) {
4749
val bottomScreen get() = selectedBackstack.second.first()
4850

@@ -120,6 +122,11 @@ private fun AppControls(model: AppModel) {
120122
Switch(model.slowAnimations, onCheckedChange = { model.slowAnimations = it })
121123
}
122124

125+
Row {
126+
Text("Inspect (pinch + drag): ", modifier = LayoutGravity.Center)
127+
Switch(model.inspectionEnabled, onCheckedChange = { model.inspectionEnabled = it })
128+
}
129+
123130
RadioGroup {
124131
model.backstacks.forEach { backstack ->
125132
RadioGroupTextItem(
@@ -143,28 +150,30 @@ private fun AppScreens(model: AppModel) {
143150
} else null
144151

145152
MaterialTheme(colors = lightColorPalette()) {
146-
Backstack(
147-
backstack = model.selectedBackstack.second,
148-
transition = model.selectedTransition.second,
149-
animationBuilder = animation,
150-
modifier = LayoutSize.Fill + DrawBorder(size = 3.dp, color = Color.Red),
151-
onTransitionStarting = { from, to, direction ->
152-
println(
153-
"""
154-
Transitioning $direction:
155-
from: $from
156-
to: $to
157-
""".trimIndent()
153+
InspectionGestureDetector(enabled = model.inspectionEnabled) {
154+
Backstack(
155+
backstack = model.selectedBackstack.second,
156+
transition = model.selectedTransition.second,
157+
animationBuilder = animation,
158+
modifier = LayoutSize.Fill + DrawBorder(size = 3.dp, color = Color.Red),
159+
onTransitionStarting = { from, to, direction ->
160+
println(
161+
"""
162+
Transitioning $direction:
163+
from: $from
164+
to: $to
165+
""".trimIndent()
166+
)
167+
},
168+
onTransitionFinished = { println("Transition finished.") }
169+
) { screen ->
170+
AppScreen(
171+
name = screen,
172+
showBack = screen != model.bottomScreen,
173+
onAdd = { model.pushScreen("$screen+") },
174+
onBack = model::popScreen
158175
)
159-
},
160-
onTransitionFinished = { println("Transition finished.") }
161-
) { screen ->
162-
AppScreen(
163-
name = screen,
164-
showBack = screen != model.bottomScreen,
165-
onAdd = { model.pushScreen("$screen+") },
166-
onBack = model::popScreen
167-
)
176+
}
168177
}
169178
}
170179
}

backstack/src/main/java/com/zachklipp/compose/backstack/Backstack.kt

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.compose.key
1111
import androidx.compose.remember
1212
import androidx.compose.state
1313
import androidx.ui.animation.animatedFloat
14+
import androidx.ui.core.AnimationClockAmbient
1415
import androidx.ui.core.ContextAmbient
1516
import androidx.ui.core.Modifier
1617
import androidx.ui.core.drawClip
@@ -103,6 +104,10 @@ private val DefaultBackstackAnimation: AnimationBuilder<Float>
103104
* @param animationBuilder Defines the curve and speed of transition animations.
104105
* @param onTransitionStarting Callback that will be invoked before starting each transition.
105106
* @param onTransitionFinished Callback that will be invoked after each transition finishes.
107+
* @param inspectionParams Optional [InspectionParams] that, when not null, enables inspection mode,
108+
* which will draw all the screens in the backstack as a translucent 3D stack. You can wrap your
109+
* backstack with [InspectionGestureDetector] to automatically generate [InspectionParams]
110+
* controlled by touch gestures.
106111
* @param drawScreen Called with each element of [backstack] to render it.
107112
*/
108113
@Composable
@@ -113,6 +118,7 @@ fun <T : Any> Backstack(
113118
animationBuilder: AnimationBuilder<Float>? = null,
114119
onTransitionStarting: ((from: List<T>, to: List<T>, TransitionDirection) -> Unit)? = null,
115120
onTransitionFinished: (() -> Unit)? = null,
121+
inspectionParams: InspectionParams? = null,
116122
drawScreen: @Composable() (T) -> Unit
117123
) {
118124
require(backstack.isNotEmpty()) { "Backstack must contain at least 1 screen." }
@@ -143,6 +149,9 @@ fun <T : Any> Backstack(
143149
}
144150
}
145151
val animation = animationBuilder ?: DefaultBackstackAnimation
152+
val clock = AnimationClockAmbient.current
153+
val inspector = remember { BackstackInspector(clock) }
154+
inspector.params = inspectionParams
146155

147156
if (direction == null && activeKeys != backstack) {
148157
// Not in the middle of a transition and we got a new backstack.
@@ -201,19 +210,14 @@ fun <T : Any> Backstack(
201210
// state as soon as a different branch is taken. See @Pivotal for more information.
202211
activeStackDrawers = remember(activeKeys, transition) {
203212
activeKeys.mapIndexed { index, key ->
204-
val isTop = index == activeKeys.size - 1
205213
ScreenWrapper(key) { progress, children ->
206-
val visibility = when {
207-
// transitionProgress always corresponds directly to visibility of the top screen.
208-
isTop -> progress
209-
// The second-to-top screen has the inverse visibility of the top screen.
210-
index == activeKeys.size - 2 -> 1f - progress
211-
// All other screens should not be drawn at all. They're only kept around to maintain
212-
// their composable state.
213-
else -> 0f
214+
// Inspector and transition are mutually exclusive.
215+
val screenModifier = if (inspector.isInspectionActive) {
216+
calculateInspectionModifier(inspector, index, activeKeys.size, progress)
217+
} else {
218+
calculateRegularModifier(transition, index, activeKeys.size, progress)
214219
}
215-
val transitionModifier = transition.modifierForScreen(visibility, isTop)
216-
Box(transitionModifier, children = children)
220+
Box(screenModifier, children = children)
217221
}
218222
}
219223
}
@@ -238,3 +242,36 @@ fun <T : Any> Backstack(
238242
}
239243
}
240244
}
245+
246+
private fun calculateRegularModifier(
247+
transition: BackstackTransition,
248+
index: Int,
249+
count: Int,
250+
progress: Float
251+
): Modifier {
252+
val visibility = when (index) {
253+
// transitionProgress always corresponds directly to visibility of the top screen.
254+
count - 1 -> progress
255+
// The second-to-top screen has the inverse visibility of the top screen.
256+
count - 2 -> 1f - progress
257+
// All other screens should not be drawn at all. They're only kept around to maintain
258+
// their composable state.
259+
else -> 0f
260+
}
261+
return transition.modifierForScreen(visibility, index == count - 1)
262+
}
263+
264+
@Composable
265+
private fun calculateInspectionModifier(
266+
inspector: BackstackInspector,
267+
index: Int,
268+
count: Int,
269+
progress: Float
270+
): Modifier {
271+
val visibility = when (index) {
272+
count - 1 -> progress
273+
// All previous screens are always visible in inspection mode.
274+
else -> 1f
275+
}
276+
return inspector.inspectScreen(index, count, visibility)
277+
}

0 commit comments

Comments
 (0)