IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 使用 Compose 实现可见 ScrollBar -> 正文阅读

[移动开发]使用 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<Measurable>, 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)
                }
            }
        })
    }
}

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-05-25 11:39:46  更:2022-05-25 11:39:57 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 0:42:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码