栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

使用 Compose 实现可见 ScrollBar

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

使用 Compose 实现可见 ScrollBar

使用 Compose 实现可见 ScrollBar
  • 前言
  • 实现效果图
  • 开始吧

前言

众所周知,如果在一个 View 内放一个更大的 View 需要滚动才能正常使用。
Compose 虽然通过嵌套可以实现水平 + 垂直的滚动,但是是不支持显示滚动条的。

尤其是使用 Compose-Desktop 的时候,是使用鼠标作为输入,而常规的滚轮只能垂直滚动,水平滚动的话是不行的,因此这里使用 Compose 实现一个水平的滚动条。

实现平台:Compose-Desktop (Compose-Android 等 Multiplatform 也是一样的)

实现效果图

数据可以随意准备,本例数据是 dump 好的 Android View Hierarchy:一个 dump.xml 文件,用于扩充 View ,一张 screenshot.png 图片用于一般展示。

开始吧

1… 解析数据略过
2… 本例实现了一个水平滚动条。
有些地方为了美观没有换行~~
滚动条宽度: 滚 动 条 宽 度 可 见 内 容 宽 度 = 可 见 内 容 宽 度 内 容 总 宽 度 frac{滚动条宽度}{可见内容宽度} = frac{可见内容宽度}{内容总宽度} 可见内容宽度滚动条宽度​=内容总宽度可见内容宽度​
定义一个 ScrollBox 方法,内一个 Box 用于包装并切割内部超长的 Box 内容

@Composable
private fun UniversalScrollBox(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
    val TAG = "UniversalScrollBox"
    Box(modifier = modifier.clipToBounds()) {
        val barWidth = remember { mutableStateOf(0) } // $frac{滚动条宽度}{可见内容宽度} = frac{可见内容宽度}{内容总宽度}$
        var dragOffset by remember { mutableStateOf(0F) }
        var widthRatio by remember { mutableStateOf(1F) } // 总宽度与可见宽度的比值
        var width by remember { mutableStateOf(0) } // 外层宽度,即内容可见的宽度
        // 如果直接在这里 .offset { IntOffset((-dragOffset * widthRatio).roundToInt(), 0) } ,那么如果 offset 超过了 width 会导致内容不可见,因此在 place 时进行 offset
        Box(modifier = Modifier.align(Alignment.TopStart)) {
            Layout(measurePolicy = object : MeasurePolicy {
                override fun MeasureScope.measure(measurables: List, constraints: Constraints): MeasureResult {
                    val placeables = measurables.map {
                        // 外层 modifier 使用 unbounded false,可以通过 constraints.maxWidth 算出外层的最大宽度
                        // 实际计算 子 Node 的时候,使用 maxWidth = Int.MAX_VALUE,可以得到 子 Node 的真实宽度
                        it.measure(constraints = constraints.copy(maxWidth = Int.MAX_VALUE))
                    }
                    width = constraints.maxWidth
                    val readWidth = placeables[0].width // 真实宽度,[0] 即为 content
                    if (readWidth > width) { // 当内容总宽度大于可见宽度时,才需要滚动条
                        widthRatio = readWidth.toFloat() / width.toFloat()
                        barWidth.value = ((width * width).toFloat() / readWidth).toInt() // 计算滚动条宽度
                    } else {
                        barWidth.value = 0
                    }
                    Log.d(TAG, "barWidth ${barWidth.value} dragOffset $dragOffset widthRatio $widthRatio offset ${(-dragOffset * widthRatio).roundToInt()} width $width readWidth $readWidth")
                    return layout(constraints.maxWidth, constraints.maxHeight) {
                        placeables.forEach {
                            it.place((-dragOffset * widthRatio).roundToInt(), 0)
                        }
                    }
                }
            }, content = {
                content()
            }, modifier = Modifier.align(Alignment.TopStart).wrapContentWidth(unbounded = false, align = Alignment.Start).offset(0.dp, 0.dp))
        }
        if (barWidth.value > 0) {
            Box(modifier = Modifier.fillMaxWidth().height(16.dp).align(Alignment.BottomStart).background(Color.Transparent)) {
                Box(modifier = Modifier.align(Alignment.BottomStart).fillMaxHeight().offset { IntOffset(dragOffset.roundToInt(), 0) }.width(barWidth.value.dp).clip(RoundedCornerShape(4.dp)) // 注意先 clip 再 background
                    .background(MaterialTheme.colors.secondary.copy(alpha = 0.5F)) // 注意先 offset 再 background
                    .draggable(state = rememberDraggableState {
                        dragOffset += it
                        if (dragOffset < 0) { // 限制拖动不要超过两端
                            dragOffset = 0F
                        } else if (dragOffset > width - barWidth.value) {
                            dragOffset = (width - barWidth.value).toFloat()
                        }
                    }, orientation = Orientation.Horizontal))
            }
        }
    }
}

3… 使用,一些类的数据结构我就不放出来了

外层

Row(modifier = Modifier.fillMaxWidth().weight(1F).padding(top = 10.dp)) {
    UniversalScrollBox(modifier = Modifier.fillMaxHeight().weight(1F).background(MaterialTheme.colors.onBackground.copy(alpha = 0.05F)).padding(4.dp)) {
        ChooseViewNode(root = r, currentChosen = c, onChosen = {
            chosenViewNode.value = it
        }, onGoingChecking = {
            goingCheckingNode.value = it
            step.value = 2
        })
    }
    Screenshot(modifier = Modifier.fillMaxHeight().weight(1F), bi, chosenViewNode = c)
}

内层

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ChooseViewNode(root: Hierarchy, currentChosen: Hierarchy, onChosen: (Hierarchy) -> Unit, onGoingChecking: (Node) -> Unit) {
    Column(modifier = Modifier.wrapContentWidth()) {
        val showChildren = remember { mutableStateOf(false) }
        Row(modifier = Modifier.wrapContentWidth().background(if (root == currentChosen) {
            MaterialTheme.colors.secondary
        } else {
            Color.Transparent
        })) {
            Icon(
                imageVector = if (showChildren.value) {
                    Icons.Default.KeyboardArrowDown
                } else {
                    if (LocalLayoutDirection.current == LayoutDirection.Ltr) Icons.Default.KeyboardArrowRight else Icons.Default.KeyboardArrowLeft
                },
                contentDescription = null,
                modifier = if ((root.children?.size ?: 0) > 0) {
                    Modifier.clickable { showChildren.value = !showChildren.value }
                } else {
                    Modifier.alpha(0.5F)
                },
            )
            val showEmptyTextDialog = remember { mutableStateOf(false) }
            ContextMenuArea(items = {
                listOf(ContextMenuItem("Check using the node") {
                    if (root !is Node) {
                        return@ContextMenuItem
                    }
                    if (root.text.isEmpty()) {
                        showEmptyTextDialog.value = true
                        return@ContextMenuItem
                    }
                    onGoingChecking(root)
                })
            }, enabled = root is Node) {
                Text(
                    text = if (root is Node) {
                        "${root.packagee} ${root.text.ifEmpty { root.clazz }}"
                    } else {
                        "Root"
                    },
                    maxLines = 1, overflow = TextOverflow.Visible, softWrap = false,
                    modifier = if ((root.children?.size ?: 0) > 0) {
                        Modifier.pointerInput(Unit) {
                            detectTapGestures(
                                onTap = {
                                    onChosen(root)
                                },
                                onDoubleTap = {
                                    showChildren.value = !showChildren.value
                                }
                            )
                        }
                    } else {
                        Modifier.clickable {
                            onChosen(root)
                        }
                    })
            }
            if (showEmptyTextDialog.value) {
                root as Node
                WrongAlertDialog(
                    "This node has empty text, please check your selected node.",
                    "${root.packagee} ${root.text.ifEmpty { root.clazz }}",
                    null
                ) {
                    showEmptyTextDialog.value = false
                }
            }
        }
        if (showChildren.value) {
            val children = root.children ?: return@Column
            if (children.size == 0) {
                return@Column
            }
            Column(modifier = Modifier.padding(start = 20.dp)) {
                children.forEach {
                    ChooseViewNode(root = it, currentChosen = currentChosen, onChosen = onChosen, onGoingChecking = onGoingChecking)
                }
            }
        }
    }
}

@Composable
private fun Screenshot(modifier: Modifier, image: BufferedImage, chosenViewNode: Hierarchy) {
    val TAG = "Screenshot"
    Box(modifier = modifier) {
        val ratio = remember { mutableStateOf(1F) }
        Layout(modifier = Modifier.align(Alignment.Center), content = content@{
            Image(painter = image.toPainter(), contentDescription = null, modifier = Modifier.fillMaxHeight())
            if (chosenViewNode !is Node) {
                return@content
            }
            val x = chosenViewNode.bounds.x
            val y = chosenViewNode.bounds.y
            val width = chosenViewNode.bounds.width
            val height = chosenViewNode.bounds.height
            Log.d(TAG, "x $x y $y width $width height $height")
            Box(modifier = Modifier.size(width = width.dp / ratio.value, height = height.dp / ratio.value).offset(x = x.dp / ratio.value, y = y.dp / ratio.value).background(Color.Transparent).border(1.dp, Color.Red))
            Log.d(TAG, "box x ${x / ratio.value} y ${y / ratio.value} width ${width / ratio.value} height ${height / ratio.value}")
        }, measurePolicy = { measurables, constraints ->
            val placeables = measurables.map {
                it.measure(constraints = constraints)
            }
            ratio.value = image.height.toFloat() / placeables[0].height // [0] 即为 Image
            Log.d(TAG, "ratio ${ratio.value} maxWidth ${constraints.maxWidth} minWidth ${constraints.minWidth}")
            layout(placeables[0].width, placeables[0].height) {
                placeables.forEach {
                    Log.d(TAG, "place height ${it.height} width ${it.width}")
                    it.place(0, 0)
                }
            }
        })
    }
}

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/931217.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号