Skip to content

Commit

Permalink
Merge pull request #65 from skiptools/scrollprecedence
Browse files Browse the repository at this point in the history
Give scrolling gestures precedence over drag
  • Loading branch information
aabewhite authored Sep 26, 2024
2 parents 142b095 + 2437a16 commit e4b9cad
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 33 deletions.
21 changes: 14 additions & 7 deletions Sources/SkipUI/SkipUI/Compose/ComposeContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,33 +43,37 @@ import androidx.compose.ui.Modifier
// because Compose's fillMax modifiers have no effect in the scroll direction. We can't use IntrinsicSize for scrolling
// containers, however
var modifier = modifier
let inheritedScrollAxes = EnvironmentValues.shared._scrollAxes
var totalScrollAxes = inheritedScrollAxes
let inheritedLayoutScrollAxes = EnvironmentValues.shared._layoutScrollAxes
var totalLayoutScrollAxes = inheritedLayoutScrollAxes
if fixedWidth || axis == .vertical {
totalScrollAxes.remove(Axis.Set.horizontal)
totalLayoutScrollAxes.remove(Axis.Set.horizontal)
}
if !fixedWidth && isFillWidth.value {
if fillWidth {
modifier = modifier.fillWidth()
} else if inheritedScrollAxes.contains(Axis.Set.horizontal) {
} else if inheritedLayoutScrollAxes.contains(Axis.Set.horizontal) {
modifier = modifier.width(IntrinsicSize.Max)
} else {
modifier = modifier.fillWidth()
}
}
if fixedHeight || axis == .horizontal {
totalScrollAxes.remove(Axis.Set.vertical)
totalLayoutScrollAxes.remove(Axis.Set.vertical)
}
if !fixedHeight && isFillHeight.value {
if fillHeight {
modifier = modifier.fillHeight()
} else if inheritedScrollAxes.contains(Axis.Set.vertical) {
} else if inheritedLayoutScrollAxes.contains(Axis.Set.vertical) {
modifier = modifier.height(IntrinsicSize.Max)
} else {
modifier = modifier.fillHeight()
}
}
totalScrollAxes.formUnion(scrollAxes)

totalLayoutScrollAxes.formUnion(scrollAxes)
let inheritedScrollAxes = EnvironmentValues.shared._scrollAxes
let totalScrollAxes = inheritedScrollAxes.union(scrollAxes)

modifier = modifier.then(then)
EnvironmentValues.shared.setValues {
// Setup the initial environment before rendering the container content
Expand All @@ -78,6 +82,9 @@ import androidx.compose.ui.Modifier
} else if eraseAxis {
$0.set_layoutAxis(nil)
}
if totalLayoutScrollAxes != inheritedLayoutScrollAxes {
$0.set_layoutScrollAxes(totalLayoutScrollAxes)
}
if totalScrollAxes != inheritedScrollAxes {
$0.set_scrollAxes(totalScrollAxes)
}
Expand Down
1 change: 0 additions & 1 deletion Sources/SkipUI/SkipUI/Compose/ComposeLayouts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ import androidx.compose.ui.unit.dp

/// Compose a view with the given frame.
@Composable func FrameLayout(view: View, context: ComposeContext, minWidth: CGFloat?, idealWidth: CGFloat?, maxWidth: CGFloat?, minHeight: CGFloat?, idealHeight: CGFloat?, maxHeight: CGFloat?, alignment: Alignment) {
let scrollAxes = EnvironmentValues.shared._scrollAxes
var thenModifier: Modifier = Modifier
if maxWidth == .infinity {
if let minWidth, minWidth > 0.0 {
Expand Down
12 changes: 11 additions & 1 deletion Sources/SkipUI/SkipUI/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,15 @@ extension EnvironmentValues {
set { setBuiltinValue(key: "_layoutAxis", value: newValue, defaultValue: { nil }) }
}

/// The axes that are unbound due to scrolling.
///
/// This is different than `_scrollAxes`, because using a fixed size container or applying a different layout axis
/// can bound a scrollable layout axis and remove it from this set.
var _layoutScrollAxes: Axis.Set {
get { builtinValue(key: "_layoutScrollAxes", defaultValue: { Axis.Set(rawValue: 0) }) as! Axis.Set }
set { setBuiltinValue(key: "_layoutScrollAxes", value: newValue, defaultValue: { Axis.Set(rawValue: 0) }) }
}

var _listItemTint: Color? {
get { builtinValue(key: "_listItemTint", defaultValue: { nil }) as! Color? }
set { setBuiltinValue(key: "_listItemTint", value: newValue, defaultValue: { nil }) }
Expand Down Expand Up @@ -520,7 +529,8 @@ extension EnvironmentValues {
set { setBuiltinValue(key: "_scrollContentBackground", value: newValue, defaultValue: { nil }) }
}

/// While `_scrollAxes` contains the effective scroll directions, this property contains the nominal directions of any ancestor scroll view.
/// While `_scrollAxes` contains the effective scroll directions, this property contains the nominal directions
/// of any ancestor scroll view.
var _scrollViewAxes: Axis.Set {
get { builtinValue(key: "_scrollViewAxes", defaultValue: { Axis.Set(rawValue: 0) }) as! Axis.Set }
set { setBuiltinValue(key: "_scrollViewAxes", value: newValue, defaultValue: { Axis.Set(rawValue: 0) }) }
Expand Down
71 changes: 47 additions & 24 deletions Sources/SkipUI/SkipUI/System/Gesture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import Foundation
#if SKIP
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.drag
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
Expand All @@ -25,7 +23,8 @@ import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import org.burnoutcrew.reorderable.awaitPointerSlopOrCancellation
import kotlin.math.abs
#endif

public protocol Gesture<V> {
Expand Down Expand Up @@ -424,7 +423,8 @@ final class GestureModifierView: ComposeModifierView {
let dragPositionX = remember { mutableStateOf(Float(0.0)) }
let dragPositionY = remember { mutableStateOf(Float(0.0)) }
let noMinimumDistance = dragGestures.value.contains { $0.minimumDistance <= 0.0 }
ret = ret.pointerInput(true) {
let scrollAxes = EnvironmentValues.shared._scrollAxes
ret = ret.pointerInput(scrollAxes) {
let onDrag: (PointerInputChange, Offset) -> Void = { change, offsetPx in
let offsetX = with(density) { offsetPx.x.toDp() }
let offsetY = with(density) { offsetPx.y.toDp() }
Expand Down Expand Up @@ -452,35 +452,58 @@ final class GestureModifierView: ComposeModifierView {
dragOffsetY.value = Float(0.0)
dragGestures.value.forEach { $0.onDragEnd(location: location, translation: translation) }
}
if noMinimumDistance {
detectDragGesturesWithoutMinimumDistance(onDrag: onDrag, onDragEnd: onDragEnd, onDragCancel: onDragCancel)
} else {
detectDragGestures(onDrag: onDrag, onDragEnd: onDragEnd, onDragCancel: onDragCancel)
}
detectDragGesturesWithScrollAxes(onDrag: onDrag, onDragEnd: onDragEnd, onDragCancel: onDragCancel, shouldAwaitTouchSlop: { !noMinimumDistance }, scrollAxes: scrollAxes)
}
}
return ret
}
}

// This is an adaptation of the internal Compose function here: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt;l=168?q=detectdraggestures&sq=
// SKIP DECLARE: suspend fun PointerInputScope.detectDragGesturesWithoutMinimumDistance(onDrag: (PointerInputChange, Offset) -> Unit, onDragEnd: () -> Unit, onDragCancel: () -> Unit)
func detectDragGesturesWithoutMinimumDistance(onDrag: (PointerInputChange, Offset) -> Void, onDragEnd: () -> Void, onDragCancel: () -> Unit) {
// This is an adaptation of the internal Compose function here: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
// SKIP DECLARE: suspend fun PointerInputScope.detectDragGesturesWithScrollAxes(onDrag: (PointerInputChange, Offset) -> Unit, onDragEnd: () -> Unit, onDragCancel: () -> Unit, shouldAwaitTouchSlop: () -> Boolean, scrollAxes: Axis.Set)
func detectDragGesturesWithScrollAxes(onDragEnd: () -> Void, onDragCancel: () -> Void, onDrag: (PointerInputChange, Offset) -> Void, shouldAwaitTouchSlop: () -> Bool, scrollAxes: Axis.Set) {
var overSlop: Offset
awaitEachGesture {
let initialDown = awaitFirstDown(requireUnconsumed: false, pass: PointerEventPass.Initial)
initialDown.consume()
let _ = awaitFirstDown(requireUnconsumed: false)

let drag: PointerInputChange = initialDown
onDrag(drag, Offset.Zero)
let upEvent = drag(pointerId: drag.id, onDrag: {
onDrag($0, $0.positionChange())
$0.consume()
})
if upEvent == nil {
onDragCancel()
let awaitTouchSlop = shouldAwaitTouchSlop()
if (!awaitTouchSlop) {
initialDown.consume()
}
let down = awaitFirstDown(requireUnconsumed: false)
var drag: PointerInputChange?
overSlop = Offset.Zero
if (awaitTouchSlop) {
repeat {
drag = awaitPointerSlopOrCancellation(down.id, down.type) { change, over in
if scrollAxes == Axis.Set.vertical {
if abs(over.x) > abs(over.y) {
change.consume()
}
} else if scrollAxes == Axis.Set.horizontal {
if abs(over.y) > abs(over.x) {
change.consume()
}
} else {
change.consume()
}
overSlop = over
}
} while drag != nil && drag?.isConsumed != true
} else {
onDragEnd()
drag = initialDown
}

if let drag {
onDrag(drag, overSlop)
let didCompleteDrag = drag(pointerId: drag.id, onDrag: {
onDrag($0, $0.positionChange())
$0.consume()
})
if didCompleteDrag {
onDragEnd()
} else {
onDragCancel()
}
}
}
}
Expand Down

0 comments on commit e4b9cad

Please sign in to comment.