Enhanced swipe-edge-oriented PredictiveBack (#317)

* Enhanced swipe-edge-oriented PredictiveBack

Following MDC NavigationDrawer design specs

* Cleanups

---------

Co-authored-by: skymkmk <whatever@tagkong.com>
This commit is contained in:
Vladimir Dostoyevskiyandskymkmk authored and GitHub committed 2023-11-23 18:01:58 +00:00
1 parent d69df5f7ab
commit ed640ab00e
3 files changed
+73 -25

No files matched your search

@@ -6,6 +6,7 @@ import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.animate
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.awaitEachGesture
@@ -15,8 +16,14 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.horizontalDrag
import androidx.compose.foundation.gestures.snapTo
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
@@ -36,9 +43,11 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
@@ -312,22 +321,41 @@ fun ModalNavigationDrawer(
val predictiveState by animateFloatMergeOneWayPredictiveBackAsState(drawerState.isOpen) {
drawerState.close()
}
val absoluteElevation = LocalAbsoluteTonalElevation.current + DrawerDefaults.ModalDrawerElevation
val predictiveModifier = if (drawerState.isOpen) {
Modifier.scale(lerp(1f, 0.9f, predictiveState)).offset {
IntOffset(lerp(0f, minValue * 0.05f, predictiveState).roundToInt(), 0)
if (predictiveState > 0) {
Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val multiplierX = lerp(1f, 1.05f, predictiveState)
val multiplierY = lerp(1f, 0.95f, predictiveState)
val scaledWidth = (multiplierX * placeable.width).roundToInt()
val scaledHeight = (multiplierY * placeable.height).roundToInt()
val reMeasured = measurable.measure(Constraints.fixed(scaledWidth, scaledHeight))
layout(scaledWidth, scaledHeight) {
reMeasured.placeRelative(scaledWidth - placeable.width, 0)
}
}.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation),
shape = DrawerDefaults.shape,
)
} else {
Modifier.scale(lerp(1f, 0.95f, -predictiveState)).offset {
IntOffset(lerp(0f, minValue * 0.05f, -predictiveState).roundToInt(), 0)
}
}
} else {
Modifier
}
Box(
modifier = Modifier.offset {
IntOffset(drawerState.requireOffset().roundToInt(), 0)
}.onGloballyPositioned {
Modifier.onGloballyPositioned {
val w = it.size.width
if (w != 0) {
minValue = -it.size.width.toFloat()
}
} then predictiveModifier,
}
}
Box(
modifier = Modifier.offset {
IntOffset(drawerState.requireOffset().roundToInt(), 0)
} then predictiveModifier.align(Alignment.CenterStart),
contentAlignment = Alignment.CenterEnd,
) {
drawerContent()
}
@@ -414,22 +442,42 @@ fun SideDrawer(
val predictiveState by animateFloatMergeOneWayPredictiveBackAsState(drawerState.isOpen) {
drawerState.close()
}
val absoluteElevation = LocalAbsoluteTonalElevation.current + DrawerDefaults.ModalDrawerElevation
val predictiveModifier = if (drawerState.isOpen) {
Modifier.scale(lerp(1f, 0.9f, predictiveState)).offset {
IntOffset(lerp(0f, minValue * 0.05f, predictiveState).roundToInt(), 0)
if (predictiveState < 0) {
Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val multiplierX = lerp(1f, 1.05f, -predictiveState)
val multiplierY = lerp(1f, 0.95f, -predictiveState)
val scaledWidth = (multiplierX * placeable.width).roundToInt()
val scaledHeight = (multiplierY * placeable.height).roundToInt()
val reMeasured = measurable.measure(Constraints.fixed(scaledWidth, scaledHeight))
layout(scaledWidth, scaledHeight) {
reMeasured.placeRelative(-(scaledWidth - placeable.width), 0)
}
}.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation),
shape = ShapeDefaults.Large.copy(topEnd = CornerSize(0), bottomEnd = CornerSize(0)),
)
} else {
Modifier.scale(lerp(1f, 0.95f, predictiveState)).offset {
IntOffset(lerp(0f, minValue * 0.05f, predictiveState).roundToInt(), 0)
}
}
} else {
Modifier
}
Box(
modifier = Modifier.offset {
IntOffset(drawerState.requireOffset().roundToInt(), 0)
}.onGloballyPositioned {
Modifier.onGloballyPositioned {
val w = it.size.width
if (w != 0) {
minValue = it.size.width.toFloat()
}
}.align(Alignment.TopEnd) then predictiveModifier,
}
}
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.End)
Box(
modifier = Modifier.offset {
IntOffset(drawerState.requireOffset().roundToInt(), 0)
}.windowInsetsPadding(insets).align(Alignment.CenterEnd) then predictiveModifier,
contentAlignment = Alignment.CenterStart,
) {
drawerContent()
}
@@ -32,17 +32,13 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.verticalScroll
@@ -354,9 +350,8 @@ class MainActivity : EhActivity() {
SideDrawer(
drawerContent = {
if (sheet != null) {
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.End)
ModalDrawerSheet(
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 112).dp).windowInsetsPadding(insets),
modifier = Modifier.widthIn(max = (configuration.screenWidthDp - 112).dp),
drawerShape = ShapeDefaults.Large.copy(topEnd = CornerSize(0), bottomEnd = CornerSize(0)),
) {
sheet(sideDrawerState)
@@ -1,5 +1,6 @@
package com.hippo.ehviewer.ui.tools
import androidx.activity.BackEventCompat
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
@@ -14,6 +15,8 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.Channel
@@ -82,11 +85,13 @@ fun animateFloatMergeOneWayPredictiveBackAsState(
onBack: suspend () -> Unit,
): State<Float> {
val animatable = remember { Animatable(0f, Float.VectorConverter) }
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
PredictiveBackHandler(enable) { progress ->
try {
progress.collect {
val isEdgeStart = (it.swipeEdge == BackEventCompat.EDGE_LEFT).xor(isRtl)
val transformed = predictiveBackInterpolator.transform(it.progress)
animatable.snapTo(transformed)
animatable.snapTo(if (isEdgeStart) transformed else -transformed)
}
} catch (e: CancellationException) {
withNonCancellableContext {