|
|
|
@ -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),
|
|
|
|
|
)
|
|
|
|
|