Skip to content

Commit

Permalink
Fix issue with triggering haptics when reaching the edge of the list. (
Browse files Browse the repository at this point in the history
…#1555)

Bug: b/294810050
  • Loading branch information
Kpeved authored Aug 7, 2023
1 parent cd82035 commit 915da38
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,7 @@ internal class PickerRotaryScrollAdapter(
*/
override fun currentItemOffset(): Float =
scrollableState.scalingLazyListState.centerItemScrollOffset.toFloat()

override fun totalItemsCount(): Int =
scrollableState.scalingLazyListState.layoutInfo.totalItemsCount
}
6 changes: 6 additions & 0 deletions compose-layout/api/current.api
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,11 @@ package com.google.android.horologist.compose.rotaryinput {

@com.google.android.horologist.annotations.ExperimentalHorologistApi public final class DefaultSnapBehavior implements com.google.android.horologist.compose.rotaryinput.RotarySnapBehavior {
ctor public DefaultSnapBehavior(com.google.android.horologist.compose.rotaryinput.RotaryScrollAdapter rotaryScrollAdapter, com.google.android.horologist.compose.rotaryinput.SnapParameters snapParameters);
method public boolean bottomEdgeReached();
method @com.google.android.horologist.annotations.ExperimentalHorologistApi public void prepareSnapForItems(int moveForElements, boolean sequentialSnap);
method public suspend Object? snapToClosestItem(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? snapToTargetItem(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public boolean topEdgeReached();
}

public final class GenericMotionRotaryInputAccumulator {
Expand Down Expand Up @@ -383,6 +385,7 @@ package com.google.android.horologist.compose.rotaryinput {
method @com.google.android.horologist.annotations.ExperimentalHorologistApi public int currentItemIndex();
method @com.google.android.horologist.annotations.ExperimentalHorologistApi public float currentItemOffset();
method public androidx.compose.foundation.gestures.ScrollableState getScrollableState();
method @com.google.android.horologist.annotations.ExperimentalHorologistApi public int totalItemsCount();
property public abstract androidx.compose.foundation.gestures.ScrollableState scrollableState;
}

Expand All @@ -395,9 +398,11 @@ package com.google.android.horologist.compose.rotaryinput {
}

@com.google.android.horologist.annotations.ExperimentalHorologistApi public interface RotarySnapBehavior {
method public boolean bottomEdgeReached();
method public void prepareSnapForItems(int moveForElements, boolean sequentialSnap);
method public suspend Object? snapToClosestItem(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? snapToTargetItem(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public boolean topEdgeReached();
}

public final class RotaryVelocityTracker {
Expand All @@ -415,6 +420,7 @@ package com.google.android.horologist.compose.rotaryinput {
method public int currentItemIndex();
method public float currentItemOffset();
method public androidx.wear.compose.foundation.lazy.ScalingLazyListState getScrollableState();
method public int totalItemsCount();
property public androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollableState;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,9 @@ public fun Modifier.rotaryWithSnap(
* An extension function for creating [RotaryScrollAdapter] from [ScalingLazyListState]
*/
@ExperimentalHorologistApi
public fun ScalingLazyListState.toRotaryScrollAdapter(): RotaryScrollAdapter =
ScalingLazyColumnRotaryScrollAdapter(this)
public fun ScalingLazyListState.toRotaryScrollAdapter(): RotaryScrollAdapter = ScalingLazyColumnRotaryScrollAdapter(
this
)

/**
* An implementation of rotary scroll adapter for [ScalingLazyColumn]
Expand All @@ -222,6 +223,11 @@ public class ScalingLazyColumnRotaryScrollAdapter(
* An offset from the item centre
*/
override fun currentItemOffset(): Float = scrollableState.centerItemScrollOffset.toFloat()

/**
* The total count of items in ScalingLazyColumn
*/
override fun totalItemsCount(): Int = scrollableState.layoutInfo.totalItemsCount
}

/**
Expand Down Expand Up @@ -253,6 +259,12 @@ public interface RotaryScrollAdapter {
*/
@ExperimentalHorologistApi
public fun currentItemOffset(): Float

/**
* The total count of items in [scrollableState]
*/
@ExperimentalHorologistApi
public fun totalItemsCount(): Int
}

/**
Expand Down Expand Up @@ -547,6 +559,16 @@ public interface RotarySnapBehavior {
*/
public suspend fun snapToClosestItem()

/**
* Returns true if top edge was reached
*/
public fun topEdgeReached(): Boolean

/**
* Returns true if bottom edge was reached
*/
public fun bottomEdgeReached(): Boolean

/**
* Performs snapping to the specified in [prepareSnapForItems] element
*/
Expand Down Expand Up @@ -600,7 +622,7 @@ public class DefaultSnapBehavior(
private val rotaryScrollAdapter: RotaryScrollAdapter,
private val snapParameters: SnapParameters
) : RotarySnapBehavior {
private var snapTarget: Int = 0
private var snapTarget: Int = rotaryScrollAdapter.currentItemIndex()
private var sequentialSnap: Boolean = false

private var anim = AnimationState(0f)
Expand All @@ -618,6 +640,7 @@ public class DefaultSnapBehavior(
snapTarget = rotaryScrollAdapter.currentItemIndex() + moveForElements
}
snapTargetUpdated = true
snapTarget = snapTarget.coerceIn(0 until rotaryScrollAdapter.totalItemsCount())
}

override suspend fun snapToClosestItem() {
Expand All @@ -637,6 +660,10 @@ public class DefaultSnapBehavior(
}
}

override fun topEdgeReached(): Boolean = snapTarget <= 0

override fun bottomEdgeReached(): Boolean = snapTarget >= rotaryScrollAdapter.totalItemsCount() - 1

override suspend fun snapToTargetItem() {
if (sequentialSnap) {
anim = anim.copy(0f)
Expand Down Expand Up @@ -882,13 +909,11 @@ internal class HighResRotaryScrollHandler(
if (rotaryFlingBehavior != null) {
flingJob.cancel()
flingJob = coroutineScope.async {
rotaryFlingBehavior?.trackFling(
beforeFling = {
debugLog { "Calling before fling section" }
scrollJob.cancel()
scrollBehavior = scrollBehaviorFactory()
}
)
rotaryFlingBehavior?.trackFling(beforeFling = {
debugLog { "Calling before fling section" }
scrollJob.cancel()
scrollBehavior = scrollBehaviorFactory()
})
}
}
}
Expand Down Expand Up @@ -1056,7 +1081,6 @@ internal class HighResSnapHandler(

val snapDistance = (snapAccumulator / snapThreshold).toInt()
.coerceIn(-maxSnapsPerEvent..maxSnapsPerEvent)
rotaryHaptics.handleSnapHaptic(event.delta)
snapAccumulator -= snapThreshold * snapDistance
val sequentialSnap = snapJob.isActive

Expand All @@ -1065,6 +1089,12 @@ internal class HighResSnapHandler(
"sequentialSnap: $sequentialSnap, " +
"snap accumulator remaining: $snapAccumulator"
}
if ((!snapBehaviour.topEdgeReached() && snapDistance < 0) ||
(!snapBehaviour.bottomEdgeReached() && snapDistance > 0)
) {
rotaryHaptics.handleSnapHaptic(event.delta)
}

snapBehaviour.prepareSnapForItems(snapDistance, sequentialSnap)
if (!snapJob.isActive) {
snapJob.cancel()
Expand Down Expand Up @@ -1238,10 +1268,9 @@ internal class ThresholdBehavior(
}

@Composable
private fun rememberTimestampChannel() =
remember {
Channel<TimestampedDelta>(capacity = Channel.CONFLATED)
}
private fun rememberTimestampChannel() = remember {
Channel<TimestampedDelta>(capacity = Channel.CONFLATED)
}

private fun exponentialSmoothing(
currentVelocity: Float,
Expand Down

0 comments on commit 915da38

Please sign in to comment.