From ed640ab00ebe22fe006e70eaf9778b0cd6d3a767 Mon Sep 17 00:00:00 2001 From: Vladimir Dostoyevskiy <99822064+VDostoyevskiy@users.noreply.github.com> Date: Fri, 24 Nov 2023 02:01:58 +0800 Subject: [PATCH] Enhanced swipe-edge-oriented PredictiveBack (#317) * Enhanced swipe-edge-oriented PredictiveBack Following MDC NavigationDrawer design specs * Cleanups --------- Co-authored-by: skymkmk --- .../compose/material3/NavigationDrawer2.kt | 84 +++++++++++++++---- .../com/hippo/ehviewer/ui/MainActivity.kt | 7 +- .../hippo/ehviewer/ui/tools/PredictiveBack.kt | 7 +- 3 files changed, 73 insertions(+), 25 deletions(-) diff --git a/app/src/main/kotlin/androidx/compose/material3/NavigationDrawer2.kt b/app/src/main/kotlin/androidx/compose/material3/NavigationDrawer2.kt index 5348d91f9..3f5e5ae1a 100644 --- a/app/src/main/kotlin/androidx/compose/material3/NavigationDrawer2.kt +++ b/app/src/main/kotlin/androidx/compose/material3/NavigationDrawer2.kt @@ -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() } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt index e71987e48..7a2738eac 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt @@ -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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PredictiveBack.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PredictiveBack.kt index 80b15b913..0183ad5da 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PredictiveBack.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PredictiveBack.kt @@ -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 { 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 {