Added clarifying comments to code and used better variable names

pull/1837/head
TJ Dahunsi 2 years ago
parent b27c1b3d0d
commit 81c6076153

@ -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 com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.ThumbState.Inactive
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
private const val INACTIVE_TO_DORMANT_COOL_DOWN = 2_000L
/** /**
* A [Scrollbar] that allows for fast scrolling of content. * A [Scrollbar] that allows for fast scrolling of content.
* Its thumb disappears when the scrolling container is dormant. * Its thumb disappears when the scrolling container is dormant.
@ -80,7 +82,7 @@ fun FastScrollbar(
orientation = orientation, orientation = orientation,
) )
}, },
onThumbMoved = onThumbMoved, onThumbDisplaced = onThumbMoved,
) )
} }
@ -202,7 +204,7 @@ private fun scrollbarThumbColor(
true -> state = Active true -> state = Active
false -> { false -> {
state = Inactive state = Inactive
delay(2_000) delay(INACTIVE_TO_DORMANT_COOL_DOWN)
state = Dormant state = Dormant
} }
} }

@ -33,7 +33,7 @@ import kotlin.math.min
* Calculates the [ScrollbarState] for lazy layouts. * Calculates the [ScrollbarState] for lazy layouts.
* @param itemsAvailable the total amount of items available to scroll in the layout. * @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 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. * as scrolling progresses for smooth and linear scrollbar thumb progression.
* [itemsAvailable]. * [itemsAvailable].
* @param reverseLayout if the items in the backing lazy layout are laid out in reverse order. * @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 : ScrollableState, LazyStateItem> LazyState.scrollbarState( internal inline fun <LazyState : ScrollableState, LazyStateItem> LazyState.scrollbarState(
itemsAvailable: Int, itemsAvailable: Int,
crossinline visibleItems: LazyState.() -> List<LazyStateItem>, crossinline visibleItems: LazyState.() -> List<LazyStateItem>,
crossinline firstItemIndex: LazyState.(List<LazyStateItem>) -> Float, crossinline firstVisibleItemIndex: LazyState.(List<LazyStateItem>) -> Float,
crossinline itemPercentVisible: LazyState.(LazyStateItem) -> Float, crossinline itemPercentVisible: LazyState.(LazyStateItem) -> Float,
crossinline reverseLayout: LazyState.() -> Boolean, crossinline reverseLayout: LazyState.() -> Boolean,
): ScrollbarState { ): ScrollbarState {
@ -58,9 +58,8 @@ internal inline fun <LazyState : ScrollableState, LazyStateItem> LazyState.scrol
val visibleItemsInfo = visibleItems(this@scrollbarState) val visibleItemsInfo = visibleItems(this@scrollbarState)
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null if (visibleItemsInfo.isEmpty()) return@snapshotFlow null
// Add the item offset for interpolation between scroll indices
val firstIndex = min( val firstIndex = min(
a = firstItemIndex(visibleItemsInfo), a = firstVisibleItemIndex(visibleItemsInfo),
b = itemsAvailable.toFloat(), b = itemsAvailable.toFloat(),
) )
if (firstIndex.isNaN()) return@snapshotFlow null if (firstIndex.isNaN()) return@snapshotFlow null
@ -77,10 +76,9 @@ internal inline fun <LazyState : ScrollableState, LazyStateItem> LazyState.scrol
a = itemsVisible / itemsAvailable, a = itemsVisible / itemsAvailable,
b = 1f, b = 1f,
) )
ScrollbarState( ScrollbarState(
thumbSizePercent = thumbSizePercent, thumbSizePercent = thumbSizePercent,
thumbTravelPercent = when { thumbDisplacementPercent = when {
reverseLayout() -> 1f - thumbTravelPercent reverseLayout() -> 1f - thumbTravelPercent
else -> thumbTravelPercent else -> thumbTravelPercent
}, },

@ -57,8 +57,8 @@ import kotlinx.coroutines.delay
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
private const val SCROLLBAR_PRESS_DELAY = 100L private const val SCROLLBAR_PRESS_DELAY = 10L
private const val SCROLLBAR_PRESS_DELTA = 0.1f private const val SCROLLBAR_PRESS_DELTA = 0.02f
/** /**
* Class definition for the core properties of a scroll bar * Class definition for the core properties of a scroll bar
@ -71,7 +71,7 @@ value class ScrollbarState internal constructor(
companion object { companion object {
val FULL = ScrollbarState( val FULL = ScrollbarState(
thumbSizePercent = 1f, thumbSizePercent = 1f,
thumbTravelPercent = 0f, thumbDisplacementPercent = 0f,
) )
} }
} }
@ -93,15 +93,16 @@ private value class ScrollbarTrack(
/** /**
* Creates a scrollbar state with the listed properties * 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 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( fun ScrollbarState(
thumbSizePercent: Float, thumbSizePercent: Float,
thumbTravelPercent: Float, thumbDisplacementPercent: Float,
) = ScrollbarState( ) = ScrollbarState(
packFloats( packFloats(
val1 = thumbSizePercent, 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 * Returns the distance the thumb has traveled as a percentage of total track size
*/ */
val ScrollbarState.thumbTravelPercent val ScrollbarState.thumbDisplacementPercent
get() = unpackFloat2(packedValue) 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 minThumbSize the minimum size of the scrollbar thumb
* @param interactionSource allows for observing the state of the scroll bar * @param interactionSource allows for observing the state of the scroll bar
* @param thumb a composable for drawing the scrollbar thumb * @param thumb a composable for drawing the scrollbar thumb
* @param onThumbMoved an function for reacting to scroll bar interactions, for example implementing * @param onThumbDisplaced an function for reacting to scroll bar displacements caused by direct
* a fast scroll * interactions on the scrollbar thumb by the user, for example implementing a fast scroll
*/ */
@Composable @Composable
fun Scrollbar( fun Scrollbar(
@ -178,50 +179,47 @@ fun Scrollbar(
minThumbSize: Dp = 40.dp, minThumbSize: Dp = 40.dp,
interactionSource: MutableInteractionSource? = null, interactionSource: MutableInteractionSource? = null,
thumb: @Composable () -> Unit, thumb: @Composable () -> Unit,
onThumbMoved: ((Float) -> Unit)? = null, onThumbDisplaced: ((Float) -> Unit)? = null,
) { ) {
val localDensity = LocalDensity.current 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 pressedOffset by remember { mutableStateOf(Offset.Unspecified) }
var draggedOffset by remember { mutableStateOf(Offset.Unspecified) } var draggedOffset by remember { mutableStateOf(Offset.Unspecified) }
var track by remember { mutableStateOf(ScrollbarTrack(0)) } // Used to immediately show drag feedback in the UI while the scrolling implementation
val updatedState by rememberUpdatedState(state) // catches up
val updatedTrack by rememberUpdatedState(track) var interactionThumbTravelPercent by remember { mutableStateOf(Float.NaN) }
var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) }
val thumbSizePercent = state.thumbSizePercent
val thumbTravelPercent = when { val thumbTravelPercent = when {
interactionThumbTravelPercent.isNaN() -> state.thumbTravelPercent interactionThumbTravelPercent.isNaN() -> state.thumbDisplacementPercent
else -> interactionThumbTravelPercent else -> interactionThumbTravelPercent
} }
val thumbSizePx = max( val thumbSizePx = max(
a = thumbSizePercent * track.size, a = state.thumbSizePercent * track.size,
b = with(localDensity) { minThumbSize.toPx() }, b = with(localDensity) { minThumbSize.toPx() },
) )
val thumbSizeDp by animateDpAsState( val thumbSizeDp by animateDpAsState(
targetValue = with(localDensity) { thumbSizePx.toDp() }, targetValue = with(localDensity) { thumbSizePx.toDp() },
label = "thumb size", label = "scrollbar thumb size",
) )
val thumbDisplacementPx = min(
val thumbTravelPx = min(
a = track.size * thumbTravelPercent, a = track.size * thumbTravelPercent,
b = track.size - thumbSizePx, b = track.size - thumbSizePx,
) )
val draggableState = rememberDraggableState { delta -> val draggableState = rememberDraggableState { delta ->
if (draggedOffset == Offset.Unspecified) return@rememberDraggableState if (draggedOffset == Offset.Unspecified) return@rememberDraggableState
draggedOffset = when (orientation) { draggedOffset = when (orientation) {
Orientation.Vertical -> draggedOffset.copy( Orientation.Vertical -> draggedOffset.copy(y = draggedOffset.y + delta)
y = draggedOffset.y + delta, Orientation.Horizontal -> draggedOffset.copy(x = draggedOffset.x + delta)
)
Orientation.Horizontal -> draggedOffset.copy(
x = draggedOffset.x + delta,
)
} }
} }
// Scrollbar track container
Box( Box(
modifier = modifier modifier = modifier
.run { .run {
@ -232,10 +230,10 @@ fun Scrollbar(
} }
} }
.onGloballyPositioned { coordinates -> .onGloballyPositioned { coordinates ->
val position = orientation.valueOf(coordinates.positionInRoot()) val scrollbarStartCoordinate = orientation.valueOf(coordinates.positionInRoot())
track = ScrollbarTrack( track = ScrollbarTrack(
max = position, max = scrollbarStartCoordinate,
min = position + orientation.valueOf(coordinates.size), min = scrollbarStartCoordinate + orientation.valueOf(coordinates.size),
) )
} }
// Process scrollbar presses // Process scrollbar presses
@ -243,17 +241,17 @@ fun Scrollbar(
detectTapGestures( detectTapGestures(
onPress = { offset -> onPress = { offset ->
val initialPress = PressInteraction.Press(offset) val initialPress = PressInteraction.Press(offset)
interactionSource?.tryEmit(initialPress) interactionSource?.tryEmit(initialPress)
// Start the press
pressedOffset = offset pressedOffset = offset
interactionSource?.tryEmit( interactionSource?.tryEmit(
if (tryAwaitRelease()) { if (tryAwaitRelease()) PressInteraction.Release(initialPress)
PressInteraction.Release(initialPress) else PressInteraction.Cancel(initialPress),
} else {
PressInteraction.Cancel(initialPress)
},
) )
// End the press
pressedOffset = Offset.Unspecified pressedOffset = Offset.Unspecified
}, },
) )
@ -271,10 +269,11 @@ fun Scrollbar(
}, },
), ),
) { ) {
val offset = max( val scrollbarThumbDisplacement = max(
a = with(localDensity) { thumbTravelPx.toDp() }, a = with(localDensity) { thumbDisplacementPx.toDp() },
b = 0.dp, b = 0.dp,
) )
// Scrollbar thumb container
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.TopStart) .align(Alignment.TopStart)
@ -287,10 +286,10 @@ fun Scrollbar(
.offset( .offset(
y = when (orientation) { y = when (orientation) {
Orientation.Horizontal -> 0.dp Orientation.Horizontal -> 0.dp
Orientation.Vertical -> offset Orientation.Vertical -> scrollbarThumbDisplacement
}, },
x = when (orientation) { x = when (orientation) {
Orientation.Horizontal -> offset Orientation.Horizontal -> scrollbarThumbDisplacement
Orientation.Vertical -> 0.dp 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 // Process presses
LaunchedEffect(pressedOffset) { LaunchedEffect(pressedOffset) {
// Press ended, reset interactionThumbTravelPercent
if (pressedOffset == Offset.Unspecified) { if (pressedOffset == Offset.Unspecified) {
interactionThumbTravelPercent = Float.NaN interactionThumbTravelPercent = Float.NaN
return@LaunchedEffect return@LaunchedEffect
} }
var currentTravel = updatedState.thumbTravelPercent var currentThumbDisplacement = updatedState.thumbDisplacementPercent
val destinationTravel = updatedTrack.thumbPosition( val destinationThumbDisplacement = track.thumbPosition(
dimension = orientation.valueOf(pressedOffset), dimension = orientation.valueOf(pressedOffset),
) )
val isPositive = currentTravel < destinationTravel val isPositive = currentThumbDisplacement < destinationThumbDisplacement
val delta = SCROLLBAR_PRESS_DELTA * if (isPositive) 1f else -1f val delta = SCROLLBAR_PRESS_DELTA * if (isPositive) 1f else -1f
while (currentTravel != destinationTravel) { while (currentThumbDisplacement != destinationThumbDisplacement) {
currentTravel = currentThumbDisplacement = when {
if (isPositive) { isPositive -> min(
min(currentTravel + delta, destinationTravel) a = currentThumbDisplacement + delta,
} else { b = destinationThumbDisplacement,
max(currentTravel + delta, destinationTravel) )
else -> max(
a = currentThumbDisplacement + delta,
b = destinationThumbDisplacement,
)
} }
onThumbMoved(currentTravel) onThumbDisplaced(currentThumbDisplacement)
interactionThumbTravelPercent = currentTravel interactionThumbTravelPercent = currentThumbDisplacement
delay(SCROLLBAR_PRESS_DELAY) delay(SCROLLBAR_PRESS_DELAY)
} }
} }
@ -334,10 +342,10 @@ fun Scrollbar(
interactionThumbTravelPercent = Float.NaN interactionThumbTravelPercent = Float.NaN
return@LaunchedEffect return@LaunchedEffect
} }
val currentTravel = updatedTrack.thumbPosition( val currentTravel = track.thumbPosition(
dimension = orientation.valueOf(draggedOffset), dimension = orientation.valueOf(draggedOffset),
) )
onThumbMoved(currentTravel) onThumbDisplaced(currentTravel)
interactionThumbTravelPercent = currentTravel interactionThumbTravelPercent = currentTravel
} }
} }

@ -37,7 +37,7 @@ fun LazyListState.scrollbarState(
scrollbarState( scrollbarState(
itemsAvailable = itemsAvailable, itemsAvailable = itemsAvailable,
visibleItems = { layoutInfo.visibleItemsInfo }, visibleItems = { layoutInfo.visibleItemsInfo },
firstItemIndex = { visibleItems -> firstVisibleItemIndex = { visibleItems ->
interpolateFirstItemIndex( interpolateFirstItemIndex(
visibleItems = visibleItems, visibleItems = visibleItems,
itemSize = { it.size }, itemSize = { it.size },
@ -71,7 +71,7 @@ fun LazyGridState.scrollbarState(
scrollbarState( scrollbarState(
itemsAvailable = itemsAvailable, itemsAvailable = itemsAvailable,
visibleItems = { layoutInfo.visibleItemsInfo }, visibleItems = { layoutInfo.visibleItemsInfo },
firstItemIndex = { visibleItems -> firstVisibleItemIndex = { visibleItems ->
interpolateFirstItemIndex( interpolateFirstItemIndex(
visibleItems = visibleItems, visibleItems = visibleItems,
itemSize = { itemSize = {

Loading…
Cancel
Save