Skip to content

Commit

Permalink
Introduce inspection mode for peering into the past.
Browse files Browse the repository at this point in the history
  • Loading branch information
zach-klippenstein committed Mar 30, 2020
1 parent b27ae49 commit 3ca9016
Show file tree
Hide file tree
Showing 6 changed files with 405 additions and 38 deletions.
Binary file added .images/inspector.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ You can use the `BackstackViewerApp` composable in the `backstack-viewer` artifa
custom transitions interactively. This composable is used by the sample app, and in the screenshots
below.

## Inspecting the backstack

The `Backstack` composable takes an optional `InspectionParams` parameter. When not null, the entire
backstack will be rendered as a translucent 3D stack. The `BackstackInspectorParams` controls how
the stack is rendered, including rotation, scaling, opacity, etc.

You can wrap your `Backstack` with the `InspectionGestureDetector` composable to automatically
control the inspector mode using touch gestures.

![Backstack inspector](.images/inspector.gif)

## Samples

There is a sample app in the `sample` module that demonstrates various transition animations and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.zachklipp.compose.backstack.Backstack
import com.zachklipp.compose.backstack.BackstackTransition
import com.zachklipp.compose.backstack.BackstackTransition.Crossfade
import com.zachklipp.compose.backstack.BackstackTransition.Slide
import com.zachklipp.compose.backstack.InspectionGestureDetector

private val DEFAULT_BACKSTACKS = listOf(
listOf("one"),
Expand All @@ -30,19 +31,20 @@ private val BUILTIN_BACKSTACK_TRANSITIONS = listOf(
"Crossfade" to Crossfade
)

@Preview
@Composable
private fun BackstackViewerAppPreview() {
BackstackViewerApp()
}
//@Preview
//@Composable
//private fun BackstackViewerAppPreview() {
// BackstackViewerApp()
//}

@Model
private class AppModel(
var namedTransitions: List<Pair<String, BackstackTransition>>,
var backstacks: List<Pair<String, List<String>>>,
var selectedTransition: Pair<String, BackstackTransition> = namedTransitions.first(),
var selectedBackstack: Pair<String, List<String>> = backstacks.first(),
var slowAnimations: Boolean = false
var slowAnimations: Boolean = false,
var inspectionEnabled: Boolean = false
) {
val bottomScreen get() = selectedBackstack.second.first()

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

Row {
Text("Inspect (pinch + drag): ", modifier = LayoutGravity.Center)
Switch(model.inspectionEnabled, onCheckedChange = { model.inspectionEnabled = it })
}

RadioGroup {
model.backstacks.forEach { backstack ->
RadioGroupTextItem(
Expand All @@ -143,28 +150,30 @@ private fun AppScreens(model: AppModel) {
} else null

MaterialTheme(colors = lightColorPalette()) {
Backstack(
backstack = model.selectedBackstack.second,
transition = model.selectedTransition.second,
animationBuilder = animation,
modifier = LayoutSize.Fill + DrawBorder(size = 3.dp, color = Color.Red),
onTransitionStarting = { from, to, direction ->
println(
"""
Transitioning $direction:
from: $from
to: $to
""".trimIndent()
InspectionGestureDetector(enabled = model.inspectionEnabled) {
Backstack(
backstack = model.selectedBackstack.second,
transition = model.selectedTransition.second,
animationBuilder = animation,
modifier = LayoutSize.Fill + DrawBorder(size = 3.dp, color = Color.Red),
onTransitionStarting = { from, to, direction ->
println(
"""
Transitioning $direction:
from: $from
to: $to
""".trimIndent()
)
},
onTransitionFinished = { println("Transition finished.") }
) { screen ->
AppScreen(
name = screen,
showBack = screen != model.bottomScreen,
onAdd = { model.pushScreen("$screen+") },
onBack = model::popScreen
)
},
onTransitionFinished = { println("Transition finished.") }
) { screen ->
AppScreen(
name = screen,
showBack = screen != model.bottomScreen,
onAdd = { model.pushScreen("$screen+") },
onBack = model::popScreen
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.key
import androidx.compose.remember
import androidx.compose.state
import androidx.ui.animation.animatedFloat
import androidx.ui.core.AnimationClockAmbient
import androidx.ui.core.ContextAmbient
import androidx.ui.core.Modifier
import androidx.ui.core.drawClip
Expand Down Expand Up @@ -103,6 +104,10 @@ private val DefaultBackstackAnimation: AnimationBuilder<Float>
* @param animationBuilder Defines the curve and speed of transition animations.
* @param onTransitionStarting Callback that will be invoked before starting each transition.
* @param onTransitionFinished Callback that will be invoked after each transition finishes.
* @param inspectionParams Optional [InspectionParams] that, when not null, enables inspection mode,
* which will draw all the screens in the backstack as a translucent 3D stack. You can wrap your
* backstack with [InspectionGestureDetector] to automatically generate [InspectionParams]
* controlled by touch gestures.
* @param drawScreen Called with each element of [backstack] to render it.
*/
@Composable
Expand All @@ -113,6 +118,7 @@ fun <T : Any> Backstack(
animationBuilder: AnimationBuilder<Float>? = null,
onTransitionStarting: ((from: List<T>, to: List<T>, TransitionDirection) -> Unit)? = null,
onTransitionFinished: (() -> Unit)? = null,
inspectionParams: InspectionParams? = null,
drawScreen: @Composable() (T) -> Unit
) {
require(backstack.isNotEmpty()) { "Backstack must contain at least 1 screen." }
Expand Down Expand Up @@ -143,6 +149,9 @@ fun <T : Any> Backstack(
}
}
val animation = animationBuilder ?: DefaultBackstackAnimation
val clock = AnimationClockAmbient.current
val inspector = remember { BackstackInspector(clock) }
inspector.params = inspectionParams

if (direction == null && activeKeys != backstack) {
// Not in the middle of a transition and we got a new backstack.
Expand Down Expand Up @@ -201,19 +210,14 @@ fun <T : Any> Backstack(
// state as soon as a different branch is taken. See @Pivotal for more information.
activeStackDrawers = remember(activeKeys, transition) {
activeKeys.mapIndexed { index, key ->
val isTop = index == activeKeys.size - 1
ScreenWrapper(key) { progress, children ->
val visibility = when {
// transitionProgress always corresponds directly to visibility of the top screen.
isTop -> progress
// The second-to-top screen has the inverse visibility of the top screen.
index == activeKeys.size - 2 -> 1f - progress
// All other screens should not be drawn at all. They're only kept around to maintain
// their composable state.
else -> 0f
// Inspector and transition are mutually exclusive.
val screenModifier = if (inspector.isInspectionActive) {
calculateInspectionModifier(inspector, index, activeKeys.size, progress)
} else {
calculateRegularModifier(transition, index, activeKeys.size, progress)
}
val transitionModifier = transition.modifierForScreen(visibility, isTop)
Box(transitionModifier, children = children)
Box(screenModifier, children = children)
}
}
}
Expand All @@ -238,3 +242,36 @@ fun <T : Any> Backstack(
}
}
}

private fun calculateRegularModifier(
transition: BackstackTransition,
index: Int,
count: Int,
progress: Float
): Modifier {
val visibility = when (index) {
// transitionProgress always corresponds directly to visibility of the top screen.
count - 1 -> progress
// The second-to-top screen has the inverse visibility of the top screen.
count - 2 -> 1f - progress
// All other screens should not be drawn at all. They're only kept around to maintain
// their composable state.
else -> 0f
}
return transition.modifierForScreen(visibility, index == count - 1)
}

@Composable
private fun calculateInspectionModifier(
inspector: BackstackInspector,
index: Int,
count: Int,
progress: Float
): Modifier {
val visibility = when (index) {
count - 1 -> progress
// All previous screens are always visible in inspection mode.
else -> 1f
}
return inspector.inspectScreen(index, count, visibility)
}
Loading

0 comments on commit 3ca9016

Please sign in to comment.