当前位置:网站首页>使用 Compose 实现可见 ScrollBar
使用 Compose 实现可见 ScrollBar
2022-07-02 06:25:00 【霏霏小雨】
前言
众所周知,如果在一个 View 内放一个更大的 View 需要滚动才能正常使用。
实现平台:Compose-Desktop (Compose-Android 等 Multiplatform 也是一样的)
实现效果图
开始吧
定义一个 UniversalScrollBox 方法,内一个 Box 用于包装所有子 Composable 内容。本例内容数据是 10 * 10 个 Text 组件。
滚动条宽度: 滚 动 条 宽 度 可 见 内 容 宽 度 = 可 见 内 容 宽 度 内 容 总 宽 度 \frac{滚动条宽度}{可见内容宽度} = \frac{可见内容宽度}{内容总宽度} 可见内容宽度滚动条宽度=内容总宽度可见内容宽度
滚动条高度:同宽度理。
private const val TAG = "UniversalScrollBox"
private fun TwoFloats(width: Float, height: Float) = androidx.compose.ui.geometry.Size(width = width, height = height)
inline val Int.ddp: Dp get() = Dp(value = this.toFloat())
inline val Float.ddp: Dp get() = Dp(value = this)
/** * 注意 content 中一定要有东西,没有做判空保护,会数组越界 */
@Composable
internal fun UniversalScrollBox(
modifier: Modifier = Modifier,
scrollBarStroke: Int = 16,
scrollBarColor: Color = MaterialTheme.colors.secondary.copy(alpha = 0.5F),
content: @Composable () -> Unit
) {
Box(
modifier = modifier
) {
var outerSize by remember {
mutableStateOf(IntSize(width = 0, height = 0)) } // 外层宽高,即内容可见的宽度
var sizeRatio by remember {
mutableStateOf(TwoFloats(width = 0F, height = 0F)) } // 内外大小比
var barSize by remember {
mutableStateOf(TwoFloats(width = 0F, height = 0F)) } // 水平滚动条宽度、垂直滚动条高度,$\frac{滚动条宽度}{可见内容宽度} = \frac{可见内容宽度}{内容总宽度}$
var dragOffset by remember {
mutableStateOf(TwoFloats(width = 0F, height = 0F)) } // 滚动条在水平、垂直方向拖动的距离
/** * 水平滚动条会遮挡垂直方向内容,因此,当水平滚动条存在时,需要设置垂直方向 padding,垂直滚动条同理。 * 当 content 变化,导致首次出现滚动条时,padding 随之发生变化,绘制时 outerSize 变化,从而可能使得另一个滚动条因此出现,继续导致 padding 变化,outerSize 变化,滚动条宽度变化。 * 但是这几次变化之后, padding 不会再改变,因而滚动条不会继续变化,变化终止。虽然第一次变化的时候,可以预测后续变化,但是懒得计算了。 */
Layout(
modifier = Modifier.fillMaxSize()
.padding(bottom = if (barSize.width > 0) 16.ddp else 0.ddp, end = if (barSize.height > 0) 16.ddp else 0.ddp)
.clipToBounds()
.align(Alignment.TopStart)
.wrapContentSize(unbounded = false),
content = content,
measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(measurables: List<Measurable>, constraints: Constraints): MeasureResult {
// 外层 modifier 使用 unbounded false,可以通过 constraints.maxWidth maxHeight 算出外层的最大大小,注意这个 Size 是 padding 之后的
outerSize = IntSize(width = constraints.maxWidth, height = constraints.maxHeight)
Log.d(TAG, "outerSize $outerSize")
val placeables = measurables.map {
measurable ->
// 实际计算 子 Node 的时候,使用 maxWidth = Int.MAX_VALUE,可以得到 子 Node 的真实宽度
measurable.measure(constraints = constraints.copy(maxHeight = Int.MAX_VALUE, maxWidth = Int.MAX_VALUE))
}
val innerSize = IntSize(width = placeables[0].width, height = placeables[0].height)
// 计算是否需要水平、垂直滚动条
val needWidth = innerSize.width > outerSize.width
val needHeight = innerSize.height > outerSize.height
sizeRatio = TwoFloats(
width = if (needWidth) innerSize.width.toFloat() / outerSize.width.toFloat() else 0F,
height = if (needHeight) innerSize.height.toFloat() / outerSize.height.toFloat() else 0F,
)
barSize = TwoFloats(
width = if (needWidth) (outerSize.width * outerSize.width).toFloat() / innerSize.width else 0F,
height = if (needHeight) (outerSize.height * outerSize.height).toFloat() / innerSize.height else 0F
)
Log.d(TAG, "bar(width: ${
barSize.width}, height ${
barSize.height}) ratio(width: ${
sizeRatio.width}, height ${
sizeRatio.height})")
return layout(width = outerSize.width, height = outerSize.height) {
placeables.forEach {
it.place(x = (-dragOffset.width * sizeRatio.width).toInt(), y = (-dragOffset.height * sizeRatio.height).toInt(), zIndex = 0F) }
}
}
}
)
if (barSize.width > 0) {
Box(
modifier = Modifier.fillMaxWidth()
.height(scrollBarStroke.ddp)
.align(Alignment.BottomStart)
.background(Color.Transparent)
) {
Box(
modifier = Modifier.align(Alignment.BottomStart)
.fillMaxHeight()
.offset {
IntOffset(dragOffset.width.roundToInt(), 0) }
.width(barSize.width.ddp)
.clip(RoundedCornerShape(4.ddp)) // 注意先 clip 再 background
.background(scrollBarColor) // 注意先 offset 再 background
.draggable(state = rememberDraggableState {
var widthOffset = dragOffset.width
widthOffset += it
// 限制拖动不要超过两端
// 注意水平、垂直滚动条会互相影响,当两方同时存在时,应该控制大小,不要相交在 BottomEnd
if (widthOffset < 0) {
widthOffset = 0F
} else if (widthOffset > outerSize.width - barSize.width) {
widthOffset = (outerSize.width - barSize.width)
}
dragOffset = TwoFloats(
width = widthOffset,
height = dragOffset.height
)
}, orientation = Orientation.Horizontal)
)
}
}
if (barSize.height > 0) {
Box(
modifier = Modifier.fillMaxHeight()
.width(scrollBarStroke.ddp)
.align(Alignment.TopEnd)
.background(Color.Transparent)
) {
Box(
modifier = Modifier.align(Alignment.TopEnd)
.fillMaxWidth()
.offset {
IntOffset(0, dragOffset.height.roundToInt()) }
.height(barSize.height.ddp)
.clip(RoundedCornerShape(4.ddp))
.background(scrollBarColor)
.draggable(state = rememberDraggableState {
var heightOffset = dragOffset.height
heightOffset += it
if (heightOffset < 0) {
heightOffset = 0F
} else if (heightOffset > outerSize.height - barSize.height) {
heightOffset = (outerSize.height - barSize.height)
}
dragOffset = TwoFloats(
width = dragOffset.width,
height = heightOffset
)
}, orientation = Orientation.Vertical)
)
}
}
}
}
3… 使用
fun main(args: Array<String>) = application {
YCRWindow(
onCloseRequest = {
exitApplication()
},
title = "Test",
state = rememberWindowState(width = 150.ddp, height = 150.ddp, position = WindowPosition.Aligned(Alignment.Center)),
icon = loadSvgPainter("love.svg")
) {
Box(
modifier = Modifier.size(100.ddp, 100.ddp)
) {
UniversalScrollBox(
modifier = Modifier.fillMaxSize()
) {
Row(modifier = Modifier.wrapContentSize()) {
repeat(10) {
Column(modifier = Modifier.wrapContentSize()) {
repeat(10) {
Text(
text = " 123 "
)
}
}
}
}
}
}
}
}
边栏推荐
猜你喜欢
@Transational踩坑
Changes in foreign currency bookkeeping and revaluation general ledger balance table (Part 2)
CAD secondary development object
Solve the problem of bindchange event jitter of swiper component of wechat applet
SQLI-LABS通關(less6-less14)
SSM实验室设备管理
Check log4j problems using stain analysis
2021-07-05c /cad secondary development create arc (4)
Principle analysis of spark
MapReduce concepts and cases (Shang Silicon Valley Learning Notes)
随机推荐
Oracle EBs and apex integrated login and principle analysis
JS countdown case
SQLI-LABS通关(less6-less14)
sqli-labs通关汇总-page4
How to call WebService in PHP development environment?
Tool grass welfare post
2021-07-05c /cad secondary development create arc (4)
In depth study of JVM bottom layer (V): class loading mechanism
数仓模型事实表模型设计
Cve - 2015 - 1635 (ms15 - 034) réplication de la vulnérabilité d'exécution de code à distance
SQL注入闭合判断
sqli-labs通关汇总-page2
外币记账及重估总账余额表变化(下)
Explanation and application of annotation and reflection
Flex Jiugongge layout
JS divides an array into groups of three
Common prototype methods of JS array
2021-07-19C#CAD二次开发创建多线段
Changes in foreign currency bookkeeping and revaluation general ledger balance table (Part 2)
Spark的原理解析