Move state into a state holder and remove redundant Box

pull/1063/head
Ben Trengrove 1 year ago
parent 01928d7df5
commit d8880e98f0

@ -38,7 +38,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -67,7 +66,7 @@ private const val SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS = 2_000L
@Composable
fun ScrollableState.DraggableScrollbar(
modifier: Modifier = Modifier,
state: State<ScrollbarState>,
state: ScrollbarState,
orientation: Orientation,
onThumbMoved: (Float) -> Unit,
) {
@ -97,7 +96,7 @@ fun ScrollableState.DraggableScrollbar(
@Composable
fun ScrollableState.DecorativeScrollbar(
modifier: Modifier = Modifier,
state: State<ScrollbarState>,
state: ScrollbarState,
orientation: Orientation,
) {
val interactionSource = remember { MutableInteractionSource() }

@ -32,9 +32,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -44,10 +44,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
@ -75,21 +75,53 @@ private const val SCROLLBAR_PRESS_DELAY_MS = 10L
*/
private const val SCROLLBAR_PRESS_DELTA_PCT = 0.02f
class ScrollbarState {
private var packedValue by mutableLongStateOf(0L)
internal fun onScroll(stateValue: ScrollbarStateValue) {
packedValue = stateValue.packedValue
}
/**
* Returns the thumb size of the scrollbar as a percentage of the total track size
*/
val thumbSizePercent
get() = unpackFloat1(packedValue)
/**
* Returns the distance the thumb has traveled as a percentage of total track size
*/
val thumbMovedPercent
get() = unpackFloat2(packedValue)
}
/**
* Returns the size of the scrollbar track in pixels
*/
private val ScrollbarTrack.size
get() = unpackFloat2(packedValue) - unpackFloat1(packedValue)
/**
* Returns the position of the scrollbar thumb on the track as a percentage
*/
private fun ScrollbarTrack.thumbPosition(
dimension: Float,
): Float = max(
a = min(
a = dimension / size,
b = 1f,
),
b = 0f,
)
/**
* Class definition for the core properties of a scroll bar
*/
@Immutable
@JvmInline
value class ScrollbarState internal constructor(
value class ScrollbarStateValue internal constructor(
internal val packedValue: Long,
) {
companion object {
val FULL = ScrollbarState(
thumbSizePercent = 1f,
thumbMovedPercent = 0f,
)
}
}
)
/**
* Class definition for the core properties of a scroll bar track
@ -106,54 +138,23 @@ private value class ScrollbarTrack(
}
/**
* Creates a [ScrollbarState] with the listed properties
* Creates a [ScrollbarStateValue] with the listed properties
* @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size.
* Refers to either the thumb width (for horizontal scrollbars)
* or height (for vertical scrollbars).
* @param thumbMovedPercent the distance the thumb has traveled as a percentage of total
* track size.
*/
fun ScrollbarState(
fun scrollbarStateValue(
thumbSizePercent: Float,
thumbMovedPercent: Float,
) = ScrollbarState(
) = ScrollbarStateValue(
packFloats(
val1 = thumbSizePercent,
val2 = thumbMovedPercent,
),
)
/**
* Returns the thumb size of the scrollbar as a percentage of the total track size
*/
val ScrollbarState.thumbSizePercent
get() = unpackFloat1(packedValue)
/**
* Returns the distance the thumb has traveled as a percentage of total track size
*/
val ScrollbarState.thumbMovedPercent
get() = unpackFloat2(packedValue)
/**
* Returns the size of the scrollbar track in pixels
*/
private val ScrollbarTrack.size
get() = unpackFloat2(packedValue) - unpackFloat1(packedValue)
/**
* Returns the position of the scrollbar thumb on the track as a percentage
*/
private fun ScrollbarTrack.thumbPosition(
dimension: Float,
): Float = max(
a = min(
a = dimension / size,
b = 1f,
),
b = 0f,
)
/**
* Returns the value of [offset] along the axis specified by [this]
*/
@ -192,14 +193,12 @@ internal fun Orientation.valueOf(intOffset: IntOffset) = when (this) {
fun Scrollbar(
modifier: Modifier = Modifier,
orientation: Orientation,
state: State<ScrollbarState>,
state: ScrollbarState,
minThumbSize: Dp = 40.dp,
interactionSource: MutableInteractionSource? = null,
thumb: @Composable () -> Unit,
onThumbMoved: ((Float) -> Unit)? = null,
) {
val localDensity = LocalDensity.current
// Using Offset.Unspecified and Float.NaN instead of null
// to prevent unnecessary boxing of primitives
var pressedOffset by remember { mutableStateOf(Offset.Unspecified) }
@ -305,11 +304,9 @@ fun Scrollbar(
},
) {
// scrollbar thumb container
Box(
modifier = Modifier
.align(Alignment.TopStart)
.layout { measurable, constraints ->
val state = state.value
Layout(content = { thumb() }) { measurables, constraints ->
val measurable = measurables.first()
val thumbSizePx = max(
a = state.thumbSizePercent * track.size,
b = minThumbSize.toPx(),
@ -325,49 +322,40 @@ fun Scrollbar(
b = track.size - thumbSizePx,
)
val scrollbarThumbMovedDp = max(
a = thumbMovedPx,
b = 0f,
val scrollbarThumbMovedPx = max(
a = thumbMovedPx.roundToInt(),
b = 0,
)
val y = when (orientation) {
Horizontal -> 0
Vertical -> scrollbarThumbMovedDp
Vertical -> scrollbarThumbMovedPx
}
val x = when (orientation) {
Horizontal -> scrollbarThumbMovedDp
Horizontal -> scrollbarThumbMovedPx
Vertical -> 0
}
val offset = IntOffset(x.toInt(), y.toInt())
val constrained = when (orientation) {
val updatedConstraints = when (orientation) {
Horizontal -> {
Constraints(
constraints.copy(
minWidth = thumbSizePx.roundToInt(),
maxWidth = thumbSizePx.roundToInt(),
minHeight = constraints.minHeight,
maxHeight = constraints.maxHeight
maxWidth = thumbSizePx.roundToInt()
)
}
Vertical -> {
Constraints(
minWidth = constraints.minWidth,
maxWidth = constraints.maxWidth,
constraints.copy(
minHeight = thumbSizePx.roundToInt(),
maxHeight = thumbSizePx.roundToInt()
)
}
}
val placeable = measurable.measure(constrained)
val placeable = measurable.measure(updatedConstraints)
layout(placeable.width, placeable.height) {
placeable.place(offset)
placeable.place(x, y)
}
}
) {
thumb()
}
}
if (onThumbMoved == null) return
@ -381,7 +369,7 @@ fun Scrollbar(
return@collect
}
var currentThumbMovedPercent = state.value.thumbMovedPercent
var currentThumbMovedPercent = state.thumbMovedPercent
val destinationThumbMovedPercent = track.thumbPosition(
dimension = orientation.valueOf(pressedOffset),
)

@ -24,8 +24,8 @@ import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
@ -41,11 +41,9 @@ import kotlin.math.min
fun LazyListState.scrollbarState(
itemsAvailable: Int,
itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index,
): State<ScrollbarState> = produceState(
initialValue = ScrollbarState.FULL,
key1 = this,
key2 = itemsAvailable,
) {
): ScrollbarState {
val state = remember { ScrollbarState() }
LaunchedEffect(this, itemsAvailable) {
snapshotFlow {
if (itemsAvailable == 0) return@snapshotFlow null
@ -81,7 +79,7 @@ fun LazyListState.scrollbarState(
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
scrollbarStateValue(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when {
layoutInfo.reverseLayout -> 1f - thumbTravelPercent
@ -91,7 +89,9 @@ fun LazyListState.scrollbarState(
}
.filterNotNull()
.distinctUntilChanged()
.collect { value = it }
.collect { state.onScroll(it) }
}
return state
}
/**
@ -104,11 +104,9 @@ fun LazyListState.scrollbarState(
fun LazyGridState.scrollbarState(
itemsAvailable: Int,
itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index,
): State<ScrollbarState> = produceState(
initialValue = ScrollbarState.FULL,
key1 = this,
key2 = itemsAvailable,
) {
): ScrollbarState {
val state = remember { ScrollbarState() }
LaunchedEffect(this, itemsAvailable) {
snapshotFlow {
if (itemsAvailable == 0) return@snapshotFlow null
@ -154,7 +152,7 @@ fun LazyGridState.scrollbarState(
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
scrollbarStateValue(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = when {
layoutInfo.reverseLayout -> 1f - thumbTravelPercent
@ -164,7 +162,9 @@ fun LazyGridState.scrollbarState(
}
.filterNotNull()
.distinctUntilChanged()
.collect { value = it }
.collect { state.onScroll(it) }
}
return state
}
/**
@ -178,11 +178,9 @@ fun LazyGridState.scrollbarState(
fun LazyStaggeredGridState.scrollbarState(
itemsAvailable: Int,
itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index,
): State<ScrollbarState> = produceState(
initialValue = ScrollbarState.FULL,
key1 = this,
key2 = itemsAvailable,
) {
): ScrollbarState {
val state = remember { ScrollbarState() }
LaunchedEffect(this, itemsAvailable) {
snapshotFlow {
if (itemsAvailable == 0) return@snapshotFlow null
@ -220,14 +218,16 @@ fun LazyStaggeredGridState.scrollbarState(
a = itemsVisible / itemsAvailable,
b = 1f,
)
ScrollbarState(
scrollbarStateValue(
thumbSizePercent = thumbSizePercent,
thumbMovedPercent = thumbTravelPercent,
)
}
.filterNotNull()
.distinctUntilChanged()
.collect { value = it }
.collect { state.onScroll(it) }
}
return state
}
private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float {

Loading…
Cancel
Save