Added clarifying comments to code and used better variable names

pull/722/head
TJ Dahunsi 2 years ago
parent bda31e9d15
commit 4858167b24

@ -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
}
}

@ -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 : ScrollableState, LazyStateItem> LazyState.scrollbarState(
itemsAvailable: Int,
crossinline visibleItems: LazyState.() -> List<LazyStateItem>,
crossinline firstItemIndex: LazyState.(List<LazyStateItem>) -> Float,
crossinline firstVisibleItemIndex: LazyState.(List<LazyStateItem>) -> Float,
crossinline itemPercentVisible: LazyState.(LazyStateItem) -> Float,
crossinline reverseLayout: LazyState.() -> Boolean,
): ScrollbarState {
@ -58,9 +58,8 @@ internal inline fun <LazyState : ScrollableState, LazyStateItem> 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 : ScrollableState, LazyStateItem> LazyState.scrol
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
thumbSizePercent = thumbSizePercent,
thumbTravelPercent = when {
thumbDisplacementPercent = when {
reverseLayout() -> 1f - thumbTravelPercent
else -> thumbTravelPercent
},

@ -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
}
}

@ -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 = {

Loading…
Cancel
Save