自定义TagViewGroup

news2024/9/24 15:24:08

来看看如何实现最最基本的TagGroupView,该视图作为ViewGroup的作用是实现其中的子视图的自动换行,保证子视图能够按照在xml中传入的顺序来相对合理地显示到界面上。

注意:其实在Android技术已经非常完善的当下,其实我们已经不怎么需要自定义ViewGroup了,因为像ConstraintsLayout这样的布局视图基本已经能解决我们**99.95%**的日常开发需求了。重复造轮子永远时开发过程中的最大忌讳,而且开源框架是经历过历史和时间的检验的,不能断言说他不会出问题,但是可以说它在绝大多数场景下不会出现问题。

先来理一理整体的思路:

  1. 工欲善其事必先利其器,我们先实现一个随机宽度,高度和颜色的视图。

  2. 实现TagGroupView

    • onMeasure中调用measureSpecWithMargin来对子视图的尺寸进行测量,存储测量结果
    • onMeasure的最后setMeasuredDimension来存储ViewGroup的测量结果
    • onLayout根据onMeasure中存储的测量结果,调用各个子视图的layout方法设置子视图的测绘范围
  3. XML文件中将若干个我们定义的子视图放置到TagGroupView中,确认实现效果

好了,基本的思路已经说完了,我们来看看每一步具体怎么实现。

步骤一:定义随机视图

定义一个能每次生成不同宽度,高度,颜色的文本视图,这一块没有什么好说的,唯一有一小点需要注意的就是随机数的生成:

  1. 种子一定要用随机的方式获取,现在的CPU性能都非常高,所以不能用当前系统毫秒时间来作为种子,这样一来所有文本视图就都一样了。
  2. 最好不断调用**nextInt()**方法来生成随机数,这样就能让实际实现的效果更加随机,也更容易发现我们开发过程中出现的问题。
class RandomTextView(context: Context, attributeSet: AttributeSet) :
    AppCompatTextView(context, attributeSet) {
    private val random by lazy { Random(System.nanoTime()) }
    private val color by lazy { Color.rgb(nextInt(), nextInt(), nextInt()) }
    private val randomWidth by lazy { nextInt() }
    private val randomHeight by lazy { nextInt() }
    private val rect by lazy { Rect() }

    init {
        text = nextInt().toString()
        textAlignment = TEXT_ALIGNMENT_CENTER
        gravity = Gravity.CENTER
        setPadding(10.dip, 10.dip, 10.dip, 10.dip)
        setBackgroundColor(color)
    }

    private fun nextInt() = random.nextInt() % 50 + 100

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        paint.getTextBounds(text.toString(), 0, text.length, rect)
        setMeasuredDimension(
            randomWidth + rect.width() + paddingLeft * 2,
            randomHeight + rect.height() + paddingTop * 2
        )
    }
}

步骤二:自定义GroupView

  • onMeasure中调用measureSpecWithMargin来对子视图的尺寸进行测量,存储测量结果

  • onMeasure的最后setMeasuredDimension来存储ViewGroup的测量结果

  • onLayout根据onMeasure中存储的测量结果,调用各个子视图的layout方法设置子视图的测绘范围


import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import kotlin.math.max

class TagGroupView(context: Context, attributeSet: AttributeSet) :
    ViewGroup(context, attributeSet) {
    //我们为每个视图定义一个[ChildViewParam]类来存储该视图的位置信息,包括其宽度,高度,起始的X,Y坐标
    private val childViewLayouts by lazy {
        0.until(childCount).map { ChildViewParam(0, 0, 0, 0) }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //已经使用的宽度
        var widthUsed = 0
        //已经使用的高度
        var heightUsed = 0
        //当前遍历行最大高度
        var maxHeightOfLine = 0
        //目前遍历的所有行的最大宽度
        var maxWidthOfLine = 0
        0.until(childCount).map { getChildAt(it) }.forEachIndexed { index, it ->
            it.layoutParams = it.layoutParams.toMarginLayoutParams
            val childViewParam = childViewLayouts[index]

            //因为我们这里仅仅是Demo,所以采用这种丑陋的抽离方式,实际上我认为应该有更好的方式,但是那个方式虽然实现看起来非常简单,但是理解成本会比较高
            //测量子视图布局方法,判断当前行放入子视图是否符合我们的规则,另外,Demo中忽略子视图设置的Margin,因为如果考虑Margin,代码会显得比较复杂
            fun doMeasureChild(): Boolean {
                //测量子视图的尺寸
                measureChildWithMargins(
                    it,
                    widthMeasureSpec,
                    widthUsed,
                    heightMeasureSpec,
                    heightUsed
                )
                //如果当前行的尺寸容许足够放入当前遍历到的子视图,刷新各个状态值,并返回true
                if (it.measuredWidth + widthUsed <= MeasureSpec.getSize(widthMeasureSpec)) {
                    childViewParam.x = widthUsed
                    childViewParam.y = heightUsed
                    childViewParam.width = it.measuredWidth
                    childViewParam.height = it.measuredHeight
                    widthUsed += it.measuredWidth
                    maxWidthOfLine = max(maxWidthOfLine, widthUsed)
                    maxHeightOfLine = max(maxHeightOfLine, it.measuredHeight)
                    return true
                }
                //否则返回false
                return false
            }
            //如果当前行空间足够,继续遍历下一个子视图
            if (doMeasureChild()) return@forEachIndexed
            //当前行空间不够,换行,重置状态值,重新尝试放入子视图
            widthUsed = 0
            heightUsed += maxHeightOfLine
            maxHeightOfLine = 0
            doMeasureChild()
        }
        //设置当前ViewGroup的测量结果
        setMeasuredDimension(maxWidthOfLine, heightUsed + maxHeightOfLine)
    }

    /**
     * 根据[onMeasure]测量子视图的结果,为每个子视图分配布局位置
     */
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        0.until(childCount).map { getChildAt(it) }.forEachIndexed { index, it ->
            val param = childViewLayouts[index]
            it.layout(param.x, param.y, param.x + param.width, param.y + param.height)
        }
    }

    private val LayoutParams.toMarginLayoutParams: MarginLayoutParams
        get() = MarginLayoutParams(this)
}

/**
 * 存储视图的起始X,Y值,宽度和高度
 */
data class ChildViewParam(var width: Int, var height: Int, var x: Int, var y: Int) {
    override fun hashCode(): Int {
        return x * x + y * y;
    }

    override fun equals(other: Any?): Boolean {
        if (other == null) return false;
        if (other is ChildViewParam)
            return x == (other as ChildViewParam).x
                    && y == (other as ChildViewParam).y;
        return false;
    }
}

步骤三:XML文件中验证对应的实现效果

<?xml version="1.0" encoding="utf-8"?>
<com.mm.android.mobilecommon.TagGroupView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.mm.android.mobilecommon.RandomTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp" />
<!--    篇幅原因,省略若干个相同定义的RandomTextView-->
</com.mm.android.mobilecommon.TagGroupView>

来看看实现的效果,可以,基本符合要求了,能自动换行,高度也符合标准了。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/739086.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【k8s】k8s的yaml文件解释,如何部署一个java.jar包

前言&#xff1a; k8s的yaml文件解释&#xff0c;如何部署一个java,jar包 YAML语法格式&#xff1a; 大小写敏感&#xff1b;使用缩进表示层级关系&#xff1b;不支持Tab键制表符缩进&#xff0c;只使用空格缩进&#xff1b;缩进的空格数目不重要&#xff0c;只要相同层级的元…

Imx6ull linux 设备树

一 linux设备树简介 1.1 linux设备树 Linux设备树是一种用于描述硬件设备信息的数据结构&#xff0c;它在Linux内核中发挥着越来越重要的作用 在系统上电后&#xff0c;BootLoader会将设备树传递给Linux内核&#xff0c;内核根据识别的树信息展开为platform_device、spi_dev…

如何用爬虫实现GPT功能

如何用爬虫实现GPT功能&#xff1f; GPT&#xff08;Generative Pre-trained Transformer&#xff09;和爬虫是两个完全不同的概念和技术。GPT是一种基于Transformer模型的自然语言处理模型&#xff0c;用于生成文本&#xff0c;而爬虫是一种用于从互联网上收集数据的技术。 …

贝叶斯 A/B 测试解释与Python实现

一、说明 A / B测试在各个行业中有许多应用。从试图确定最佳市场群体到医疗药物测试&#xff0c;它具有各种应用&#xff0c;并允许企业根据结果做出决策。有两种常见的A/B检验方法&#xff0c;频率主义方法和贝叶斯方法&#xff0c;两者都是从假设检验的基础出发的。在本文中&…

819. 递归求阶乘

链接&#xff1a; 819.递归求阶乘 题目&#xff1a; 请使用递归的方式求 nn 的阶乘。 输入格式 共一行&#xff0c;包含一个整数 nn。 输出格式 共一行&#xff0c;包含一个整数&#xff0c;表示 nn 的阶乘的值。 数据范围 1≤n≤101≤n≤10 输入样例&#xff1a; 3输出样例&am…

【Linux】查看系统各种信息的常用命令 (CPU、内存、进程、网口、磁盘、硬件、等等)

Linux是一种开源的类Unix操作系统&#xff0c;它有很多不同的发行版&#xff0c;如Ubuntu、CentOS、Debian等。Linux系统提供了很多命令行工具&#xff0c;可以让用户方便地查看和管理系统的各种信息&#xff0c;如硬件配置、内存使用、进程状态、网络连接等。本文将介绍一些常…

解决IDEA项目external libraries依赖包消失的问题

有时候电脑重启后&#xff0c;再打开IDEA上的项目时会出现external libraries目录下的依赖包都消失了的情况&#xff0c;只剩下了一个JDK的包 网上说可以通过刷新IDEA的缓存解决&#xff0c;但我试了没有效果&#xff0c;最后使用如下办法解决&#xff1a; 1.删除项目目录下的…

python接口自动化(二十五)--unittest断言——下(详解)

简介 本篇还是回归到我们最初始的话题&#xff0c;想必大家都忘记了&#xff0c;没关系看这里&#xff1a;传送门 没错最初的话题就是登录&#xff0c;由于博客园的登录机制改变了&#xff0c;本篇以我找到的开源免费的登录API为案例&#xff0c;结合 unittest 框架写 2 个用例…

MySQL原理探索——26 备库为什么会延迟好几个小时

在上一篇文章中&#xff0c;介绍了几种可能导致备库延迟的原因。你会发现&#xff0c;这些场景里&#xff0c;不论是偶发性的查询压力&#xff0c;还是备份&#xff0c;对备库延迟的影响一般是分钟级的&#xff0c;而且在备库恢复正常以后都能够追上来。 但是&#xff0c;如果备…

nvm的安装与使用5分钟极速上手

nvm的安装与使用5分钟极速上手 下载 nvm 并安装 nvm官网下载地址推荐下载 nvm-setup.zip 这一个&#xff0c;nvm-noinstall.zip下载之后不用安装&#xff0c;但是得自己配置setting.txt文件&#xff0c;以及环境变量&#xff0c;过于麻烦。nvm-setup.zip 会直接帮我们把环境变…

开篇(变量、运算符、进制)

一、Java简介 JDK&#xff08;JRE&#xff08;JVM & API&#xff09;、Java language&#xff08;tools & APIs&#xff09;&#xff09;&#xff1b;java源文件 → javac的编译 → class类&#xff08;会生成一个或多个class文件&#xff09;&#xff1b; * 使用 jav…

什么是Qt Widgets?一组创建经典桌面应用UI的界面组件!

Qt 是目前最先进、最完整的跨平台C开发工具。它不仅完全实现了一次编写&#xff0c;所有平台无差别运行&#xff0c;更提供了几乎所有开发过程中需要用到的工具。如今&#xff0c;Qt已被运用于超过70个行业、数千家企业&#xff0c;支持数百万设备及应用。 Qt Widgets模块提供…

QGIS 根据点位批量出图

背景 在工作中&#xff0c;当有大量项目点位需要结合地图介绍时&#xff0c;则需要批量截图。于是有了今天的教程。 一 工具及材料准备 QGIS 插件 QuickMapServices 点位集合 CSV文件 其他图层文件 二 导入图层 这里根据实际需要导入自己对应格式的就好。 图层-添加…

计算机网络地址

1、ipv4地址 2、网络地址转换 3、子网划分和主机号

Spring MVC 注解实现

注解描述 注解描述Controller用于标记在一个类上&#xff0c;使用它标记的类就是一个SpringMVC Controller 对象&#xff0c;分发处理器将会扫描使用了该注解的类的方法&#xff0c;并检测该方法是否使用了RequestMapping 注解。Controller 只是定义了一个控制器类&#xff0c…

第七章 环境软件的安装

1、nodeJS安装 node -v node版本 安装多个node版本 pnpm i nvm -g nvm -v nvm ls 查看当前安装的版本 nvm install 18.7.0 安装指定的版本 nvm use 18.6.0 切换到别的版本 安装pnpm npm i pnpm -g pnpm -v 安装VSCode 官网直接下载 安装好后 需要配置 按住 CMD +…

基于单片机的老人防摔倒的设计与实现

功能介绍 以51单片机作为主控系统&#xff1b;通过LCD1602液晶显示屏显示当前的经纬度及时间的信息&#xff1b;温度传感器采集当前体温&#xff1b;通过GPS接收模块获得当前位置的位置的经度、纬度、时间和高度等信息&#xff1b;通过ADXL345检测老人摔倒的一瞬间重力加速度通…

[解决Github 克隆错误] unable to access ‘xxx‘: Recv failure: Connect

1.错误描述&#xff1a; 从GitHub上克隆仓库到本地&#xff0c;出现错误&#xff1a; unable to access https://github.com/xxxx: Recv failure: Connection was reset。 克隆失败。 2.第一次解决此问题&#xff1a;从终端输入&#xff1a; git config --global http.sslVer…

Liunx下的消费者与生产者模型与简单线程池的实现

文章目录 前言1.消费者与生产者模型2.信号量1.信号量的接口2.使用环形队列模拟生产者消费者模型 3.简单实现线程池4.补充说明 前言 本文主要会结束消费者生产者模型&#xff0c;以及简单线程池的实现。 1.消费者与生产者模型 之前我们学了条件变量和互斥等概念。条件变量本质…

vETSTStudio - CAPL - CAN报文未使用位

目录 ChkStart_PayloadGapsObservation 代码示例 ChkStart_PayloadGapsObservationTx 代码示例 ChkStart_PayloadGapsObservationRx 代码示例 我们在做CAN&CANFD通信或者CAN&CANFD网络管理的时候&#xff0c;就会测试到DBC中报文各种信号和位的使用状态&#xff…