Skip to content

Commit d4e8e20

Browse files
committed
Connect drawer: own the drawer so its gestures and the content's stay apart
The Material sheet could not keep the drawer's open/close gesture and the content's scroll separate: while its settle animation runs it consumes any touch's down for its own drag, so a scroll that started right after an opening flick went to a sheet already at the top and died there, and a pull through the content fought the animation frame by frame so only a fast flick closed the drawer. Nothing in the sheet state can move, snap or interrupt it from outside. Replace the scaffold with the drawer on foundation's anchored draggable (ConnectDrawerState): the same peek geometry, two-thirds expanded height, fold marker, insets, tablet panel, handle and reset-on-close as before, with the anchors computed from the layout, the peek and the measured sheet. The drag handle keeps its own drag (and grabs a moving drawer), with the expand/collapse actions for assistive tech. Under the content, one nested-scroll connection routes each touch by ConnectSheetGesture's kind: a drawer gesture interrupts any running animation, moves the drawer by raw deltas, settles on release by the handle's own rule (125 dp/s, then 56 dp from where it rested) and never scrolls the content; a content gesture scrolls the content, stops at its top and never moves the drawer; the content's own flings pass through untouched. The release rule is pure and unit tested. Verified on the S21: a scroll and a drag started right after an opening flick, a slow pull to close from rest, during the opening animation and right after a content fling, a flick to close, a long opening drag past the top, a content fling that reaches the top, and opening and closing by the handle; the content is at its top after every close.
1 parent 67f76b8 commit d4e8e20

5 files changed

Lines changed: 337 additions & 90 deletions

File tree

app/app/src/main/java/com/bringyour/network/ui/MainNavHost.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ import androidx.compose.material3.Icon
4747
import androidx.compose.material3.NavigationBarItemDefaults
4848
import androidx.compose.material3.NavigationDrawerItemDefaults
4949
import androidx.compose.material3.NavigationRailItemDefaults
50-
import androidx.compose.material3.SheetState
50+
import com.bringyour.network.ui.connect.ConnectDrawerState
51+
import com.bringyour.network.ui.connect.rememberConnectDrawerState
5152
import androidx.compose.material3.SheetValue
5253
import androidx.compose.material3.VerticalDivider
5354
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
@@ -264,10 +265,7 @@ private fun MainNavHostContent(
264265
val scope = rememberCoroutineScope()
265266

266267
// hoisted here so re-tapping the connect tab can collapse it
267-
val connectActionsSheetState = rememberStandardBottomSheetState(
268-
initialValue = SheetValue.PartiallyExpanded,
269-
skipHiddenState = true
270-
)
268+
val connectActionsSheetState = rememberConnectDrawerState()
271269

272270
val navSuiteLayoutType = with(adaptiveInfo) {
273271

@@ -1083,7 +1081,7 @@ fun MainNavContent(
10831081
solanaPaymentViewModel: SolanaPaymentViewModel,
10841082
isCheckingSolanaTransaction: Boolean,
10851083
isPro: Boolean,
1086-
connectActionsSheetState: SheetState,
1084+
connectActionsSheetState: ConnectDrawerState,
10871085
accountViewModel: AccountViewModel = hiltViewModel<AccountViewModel>(),
10881086
profileViewModel: ProfileViewModel = hiltViewModel<ProfileViewModel>(),
10891087
accountPointsViewModel: AccountPointsViewModel = hiltViewModel<AccountPointsViewModel>(),

app/app/src/main/java/com/bringyour/network/ui/connect/BrowseLocationsScreen.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
1111
import androidx.compose.material3.Icon
1212
import androidx.compose.material3.IconButton
1313
import androidx.compose.material3.Scaffold
14-
import androidx.compose.material3.SheetState
1514
import androidx.compose.material3.Text
1615
import androidx.compose.material3.TopAppBarDefaults
1716
import androidx.compose.runtime.Composable
@@ -36,7 +35,7 @@ fun BrowseLocationsScreen(
3635
locationsListViewModel: LocationsListViewModel,
3736
connectViewModel: ConnectViewModel,
3837
navController: NavController,
39-
connectActionsSheetState: SheetState,
38+
connectActionsSheetState: ConnectDrawerState,
4039
networkPeersViewModel: NetworkPeersViewModel = hiltViewModel(),
4140
) {
4241
val fetchLocationsState by remember { locationsListViewModel.filterLocationsState }.collectAsState()
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.bringyour.network.ui.connect
2+
3+
import androidx.compose.foundation.MutatePriority
4+
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
5+
import androidx.compose.foundation.gestures.AnchoredDraggableState
6+
import androidx.compose.foundation.gestures.animateTo
7+
import androidx.compose.foundation.gestures.animateToWithDecay
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.runtime.Stable
10+
import androidx.compose.runtime.remember
11+
import kotlin.math.abs
12+
13+
/** The connect drawer's two resting positions. */
14+
enum class ConnectDrawerValue { Collapsed, Expanded }
15+
16+
/**
17+
* The connect drawer's position, on foundation's anchored draggable so the
18+
* drawer, its drag handle and the nested-scroll connection under its content
19+
* all move the same state and the gesture rules (mmm/DESIGNSTYLE.md) can be
20+
* enforced in full: a Material sheet would grab any touch that lands while
21+
* it animates, and gives no way to move or snap it from outside.
22+
*/
23+
@Stable
24+
class ConnectDrawerState(initialValue: ConnectDrawerValue = ConnectDrawerValue.Collapsed) {
25+
26+
val draggable = AnchoredDraggableState(initialValue)
27+
28+
val currentValue: ConnectDrawerValue
29+
get() = draggable.currentValue
30+
31+
/** Where the drawer is heading: its resting position, or the one it animates to. */
32+
val targetValue: ConnectDrawerValue
33+
get() = draggable.targetValue
34+
35+
val isAnimationRunning: Boolean
36+
get() = draggable.isAnimationRunning
37+
38+
fun requireOffset(): Float = draggable.requireOffset()
39+
40+
suspend fun expand() {
41+
draggable.animateTo(ConnectDrawerValue.Expanded, AnchoredDraggableDefaults.SnapAnimationSpec)
42+
}
43+
44+
suspend fun partialExpand() {
45+
draggable.animateTo(ConnectDrawerValue.Collapsed, AnchoredDraggableDefaults.SnapAnimationSpec)
46+
}
47+
48+
/**
49+
* Stops a running animation so a drag through the content moves the
50+
* drawer from where it is, instead of fighting the animation frame by
51+
* frame.
52+
*/
53+
suspend fun interruptAnimation() {
54+
if (draggable.isAnimationRunning) {
55+
draggable.anchoredDrag(MutatePriority.UserInput) { }
56+
}
57+
}
58+
59+
/**
60+
* Settles the drawer after a drag through its content was released with
61+
* [velocity] (negative upward), by the same rule as the drag handle: a
62+
* release faster than [velocityThresholdPx] goes the way it moves, a
63+
* slower one goes to the other position only once it has travelled
64+
* [positionalThresholdPx] from where it rested.
65+
*/
66+
suspend fun release(velocity: Float, velocityThresholdPx: Float, positionalThresholdPx: Float) {
67+
val anchors = draggable.anchors
68+
if (!anchors.hasPositionFor(ConnectDrawerValue.Expanded) || !anchors.hasPositionFor(ConnectDrawerValue.Collapsed)) {
69+
return
70+
}
71+
val target = releaseTarget(
72+
offset = requireOffset(),
73+
velocity = velocity,
74+
from = draggable.settledValue,
75+
expandedOffset = anchors.positionOf(ConnectDrawerValue.Expanded),
76+
collapsedOffset = anchors.positionOf(ConnectDrawerValue.Collapsed),
77+
velocityThresholdPx = velocityThresholdPx,
78+
positionalThresholdPx = positionalThresholdPx,
79+
)
80+
draggable.animateToWithDecay(target, velocity)
81+
}
82+
83+
companion object {
84+
85+
/** The resting position a release lands on. Pure, for tests. */
86+
fun releaseTarget(
87+
offset: Float,
88+
velocity: Float,
89+
from: ConnectDrawerValue,
90+
expandedOffset: Float,
91+
collapsedOffset: Float,
92+
velocityThresholdPx: Float,
93+
positionalThresholdPx: Float,
94+
): ConnectDrawerValue {
95+
if (abs(velocity) >= velocityThresholdPx) {
96+
return if (velocity < 0f) ConnectDrawerValue.Expanded else ConnectDrawerValue.Collapsed
97+
}
98+
return when (from) {
99+
ConnectDrawerValue.Collapsed ->
100+
if (offset <= collapsedOffset - positionalThresholdPx) ConnectDrawerValue.Expanded
101+
else ConnectDrawerValue.Collapsed
102+
ConnectDrawerValue.Expanded ->
103+
if (offset >= expandedOffset + positionalThresholdPx) ConnectDrawerValue.Collapsed
104+
else ConnectDrawerValue.Expanded
105+
}
106+
}
107+
}
108+
}
109+
110+
@Composable
111+
fun rememberConnectDrawerState(): ConnectDrawerState {
112+
return remember { ConnectDrawerState() }
113+
}

0 commit comments

Comments
 (0)