From 81c60761532b9419d14a39cb65d841df23e84adf Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Sat, 13 May 2023 12:21:34 +0100 Subject: [PATCH] Added clarifying comments to code and used better variable names --- .../component/scrollbar/AppScrollbars.kt | 6 +- .../scrollbar/LazyScrollbarUtilities.kt | 10 +- .../component/scrollbar/Scrollbar.kt | 122 ++++++++++-------- .../component/scrollbar/ScrollbarExt.kt | 4 +- 4 files changed, 75 insertions(+), 67 deletions(-) diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt index 2a8827c3d..2786a09fe 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/AppScrollbars.kt @@ -49,6 +49,8 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollba import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Inactive import kotlinx.coroutines.delay +private const val INACTIVE_TO_DORMANT_COOL_DOWN = 2_000L + /** * A [Scrollbar] that allows for fast scrolling of content. * Its thumb disappears when the scrolling container is dormant. @@ -80,7 +82,7 @@ fun FastScrollbar( orientation = orientation, ) }, - onThumbMoved = onThumbMoved, + onThumbDisplaced = onThumbMoved, ) } @@ -202,7 +204,7 @@ private fun scrollbarThumbColor( true -> state = Active false -> { state = Inactive - delay(2_000) + delay(INACTIVE_TO_DORMANT_COOL_DOWN) state = Dormant } } diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt index d45c5781a..c4ce8c22d 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt @@ -33,7 +33,7 @@ import kotlin.math.min * Calculates the [ScrollbarState] for lazy layouts. * @param itemsAvailable the total amount of items available to scroll in the layout. * @param visibleItems a list of items currently visible in the layout. - * @param firstItemIndex a function for interpolating the first visible index in the lazy layout + * @param firstVisibleItemIndex a function for interpolating the first visible index in the lazy layout * as scrolling progresses for smooth and linear scrollbar thumb progression. * [itemsAvailable]. * @param reverseLayout if the items in the backing lazy layout are laid out in reverse order. @@ -42,7 +42,7 @@ import kotlin.math.min internal inline fun LazyState.scrollbarState( itemsAvailable: Int, crossinline visibleItems: LazyState.() -> List, - crossinline firstItemIndex: LazyState.(List) -> Float, + crossinline firstVisibleItemIndex: LazyState.(List) -> Float, crossinline itemPercentVisible: LazyState.(LazyStateItem) -> Float, crossinline reverseLayout: LazyState.() -> Boolean, ): ScrollbarState { @@ -58,9 +58,8 @@ internal inline fun LazyState.scrol val visibleItemsInfo = visibleItems(this@scrollbarState) if (visibleItemsInfo.isEmpty()) return@snapshotFlow null - // Add the item offset for interpolation between scroll indices val firstIndex = min( - a = firstItemIndex(visibleItemsInfo), + a = firstVisibleItemIndex(visibleItemsInfo), b = itemsAvailable.toFloat(), ) if (firstIndex.isNaN()) return@snapshotFlow null @@ -77,10 +76,9 @@ internal inline fun LazyState.scrol a = itemsVisible / itemsAvailable, b = 1f, ) - ScrollbarState( thumbSizePercent = thumbSizePercent, - thumbTravelPercent = when { + thumbDisplacementPercent = when { reverseLayout() -> 1f - thumbTravelPercent else -> thumbTravelPercent }, diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt index 3d33247d9..5baf003e0 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/Scrollbar.kt @@ -57,8 +57,8 @@ import kotlinx.coroutines.delay import kotlin.math.max import kotlin.math.min -private const val SCROLLBAR_PRESS_DELAY = 100L -private const val SCROLLBAR_PRESS_DELTA = 0.1f +private const val SCROLLBAR_PRESS_DELAY = 10L +private const val SCROLLBAR_PRESS_DELTA = 0.02f /** * Class definition for the core properties of a scroll bar @@ -71,7 +71,7 @@ value class ScrollbarState internal constructor( companion object { val FULL = ScrollbarState( thumbSizePercent = 1f, - thumbTravelPercent = 0f, + thumbDisplacementPercent = 0f, ) } } @@ -93,15 +93,16 @@ private value class ScrollbarTrack( /** * Creates a scrollbar state with the listed properties * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size - * @param thumbTravelPercent the distance the thumb has traveled as a percentage of total track size + * @param thumbDisplacementPercent the distance the thumb has traveled as a percentage of total + * track size */ fun ScrollbarState( thumbSizePercent: Float, - thumbTravelPercent: Float, + thumbDisplacementPercent: Float, ) = ScrollbarState( packFloats( val1 = thumbSizePercent, - val2 = thumbTravelPercent, + val2 = thumbDisplacementPercent, ), ) @@ -114,7 +115,7 @@ val ScrollbarState.thumbSizePercent /** * Returns the distance the thumb has traveled as a percentage of total track size */ -val ScrollbarState.thumbTravelPercent +val ScrollbarState.thumbDisplacementPercent get() = unpackFloat2(packedValue) /** @@ -167,8 +168,8 @@ internal fun Orientation.valueOf(intOffset: IntOffset) = when (this) { * @param minThumbSize the minimum size of the scrollbar thumb * @param interactionSource allows for observing the state of the scroll bar * @param thumb a composable for drawing the scrollbar thumb - * @param onThumbMoved an function for reacting to scroll bar interactions, for example implementing - * a fast scroll + * @param onThumbDisplaced an function for reacting to scroll bar displacements caused by direct + * interactions on the scrollbar thumb by the user, for example implementing a fast scroll */ @Composable fun Scrollbar( @@ -178,50 +179,47 @@ fun Scrollbar( minThumbSize: Dp = 40.dp, interactionSource: MutableInteractionSource? = null, thumb: @Composable () -> Unit, - onThumbMoved: ((Float) -> Unit)? = null, + onThumbDisplaced: ((Float) -> Unit)? = null, ) { val localDensity = LocalDensity.current - var interactionThumbTravelPercent by remember { mutableStateOf(Float.NaN) } + + // Using Offset.Unspecified and Float.NaN instead of null + // to prevent unnecessary boxing of primitives var pressedOffset by remember { mutableStateOf(Offset.Unspecified) } var draggedOffset by remember { mutableStateOf(Offset.Unspecified) } - var track by remember { mutableStateOf(ScrollbarTrack(0)) } - val updatedState by rememberUpdatedState(state) - val updatedTrack by rememberUpdatedState(track) + // Used to immediately show drag feedback in the UI while the scrolling implementation + // catches up + var interactionThumbTravelPercent by remember { mutableStateOf(Float.NaN) } + + var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) } - val thumbSizePercent = state.thumbSizePercent val thumbTravelPercent = when { - interactionThumbTravelPercent.isNaN() -> state.thumbTravelPercent + interactionThumbTravelPercent.isNaN() -> state.thumbDisplacementPercent else -> interactionThumbTravelPercent } val thumbSizePx = max( - a = thumbSizePercent * track.size, + a = state.thumbSizePercent * track.size, b = with(localDensity) { minThumbSize.toPx() }, ) - val thumbSizeDp by animateDpAsState( targetValue = with(localDensity) { thumbSizePx.toDp() }, - label = "thumb size", + label = "scrollbar thumb size", ) - - val thumbTravelPx = min( + val thumbDisplacementPx = min( a = track.size * thumbTravelPercent, b = track.size - thumbSizePx, ) - val draggableState = rememberDraggableState { delta -> if (draggedOffset == Offset.Unspecified) return@rememberDraggableState draggedOffset = when (orientation) { - Orientation.Vertical -> draggedOffset.copy( - y = draggedOffset.y + delta, - ) - - Orientation.Horizontal -> draggedOffset.copy( - x = draggedOffset.x + delta, - ) + Orientation.Vertical -> draggedOffset.copy(y = draggedOffset.y + delta) + Orientation.Horizontal -> draggedOffset.copy(x = draggedOffset.x + delta) } } + + // Scrollbar track container Box( modifier = modifier .run { @@ -232,10 +230,10 @@ fun Scrollbar( } } .onGloballyPositioned { coordinates -> - val position = orientation.valueOf(coordinates.positionInRoot()) + val scrollbarStartCoordinate = orientation.valueOf(coordinates.positionInRoot()) track = ScrollbarTrack( - max = position, - min = position + orientation.valueOf(coordinates.size), + max = scrollbarStartCoordinate, + min = scrollbarStartCoordinate + orientation.valueOf(coordinates.size), ) } // Process scrollbar presses @@ -243,17 +241,17 @@ fun Scrollbar( detectTapGestures( onPress = { offset -> val initialPress = PressInteraction.Press(offset) - interactionSource?.tryEmit(initialPress) + + // Start the press pressedOffset = offset interactionSource?.tryEmit( - if (tryAwaitRelease()) { - PressInteraction.Release(initialPress) - } else { - PressInteraction.Cancel(initialPress) - }, + if (tryAwaitRelease()) PressInteraction.Release(initialPress) + else PressInteraction.Cancel(initialPress), ) + + // End the press pressedOffset = Offset.Unspecified }, ) @@ -271,10 +269,11 @@ fun Scrollbar( }, ), ) { - val offset = max( - a = with(localDensity) { thumbTravelPx.toDp() }, + val scrollbarThumbDisplacement = max( + a = with(localDensity) { thumbDisplacementPx.toDp() }, b = 0.dp, ) + // Scrollbar thumb container Box( modifier = Modifier .align(Alignment.TopStart) @@ -287,10 +286,10 @@ fun Scrollbar( .offset( y = when (orientation) { Orientation.Horizontal -> 0.dp - Orientation.Vertical -> offset + Orientation.Vertical -> scrollbarThumbDisplacement }, x = when (orientation) { - Orientation.Horizontal -> offset + Orientation.Horizontal -> scrollbarThumbDisplacement Orientation.Vertical -> 0.dp }, ), @@ -299,31 +298,40 @@ fun Scrollbar( } } - if (onThumbMoved == null) return + if (onThumbDisplaced == null) return + + // State that will be read inside the effects that follow + // but will not cause re-triggering of them + val updatedState by rememberUpdatedState(state) // Process presses LaunchedEffect(pressedOffset) { + // Press ended, reset interactionThumbTravelPercent if (pressedOffset == Offset.Unspecified) { interactionThumbTravelPercent = Float.NaN return@LaunchedEffect } - var currentTravel = updatedState.thumbTravelPercent - val destinationTravel = updatedTrack.thumbPosition( + var currentThumbDisplacement = updatedState.thumbDisplacementPercent + val destinationThumbDisplacement = track.thumbPosition( dimension = orientation.valueOf(pressedOffset), ) - val isPositive = currentTravel < destinationTravel + val isPositive = currentThumbDisplacement < destinationThumbDisplacement val delta = SCROLLBAR_PRESS_DELTA * if (isPositive) 1f else -1f - while (currentTravel != destinationTravel) { - currentTravel = - if (isPositive) { - min(currentTravel + delta, destinationTravel) - } else { - max(currentTravel + delta, destinationTravel) - } - onThumbMoved(currentTravel) - interactionThumbTravelPercent = currentTravel + while (currentThumbDisplacement != destinationThumbDisplacement) { + currentThumbDisplacement = when { + isPositive -> min( + a = currentThumbDisplacement + delta, + b = destinationThumbDisplacement, + ) + else -> max( + a = currentThumbDisplacement + delta, + b = destinationThumbDisplacement, + ) + } + onThumbDisplaced(currentThumbDisplacement) + interactionThumbTravelPercent = currentThumbDisplacement delay(SCROLLBAR_PRESS_DELAY) } } @@ -334,10 +342,10 @@ fun Scrollbar( interactionThumbTravelPercent = Float.NaN return@LaunchedEffect } - val currentTravel = updatedTrack.thumbPosition( + val currentTravel = track.thumbPosition( dimension = orientation.valueOf(draggedOffset), ) - onThumbMoved(currentTravel) + onThumbDisplaced(currentTravel) interactionThumbTravelPercent = currentTravel } } diff --git a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt index aea4cd661..26f0bb2ae 100644 --- a/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt +++ b/core/designsystem/src/main/java/com/google/samples/apps/nowinandroid/core/designsystem/component/scrollbar/ScrollbarExt.kt @@ -37,7 +37,7 @@ fun LazyListState.scrollbarState( scrollbarState( itemsAvailable = itemsAvailable, visibleItems = { layoutInfo.visibleItemsInfo }, - firstItemIndex = { visibleItems -> + firstVisibleItemIndex = { visibleItems -> interpolateFirstItemIndex( visibleItems = visibleItems, itemSize = { it.size }, @@ -71,7 +71,7 @@ fun LazyGridState.scrollbarState( scrollbarState( itemsAvailable = itemsAvailable, visibleItems = { layoutInfo.visibleItemsInfo }, - firstItemIndex = { visibleItems -> + firstVisibleItemIndex = { visibleItems -> interpolateFirstItemIndex( visibleItems = visibleItems, itemSize = {