直播间自定义公屏视图的升级之路(View版)

news2024/11/19 6:23:45

目录

  • 1.前言
  • 2.旧版设计的分析
  • 3.新版设计的分析
  • 4.代码实现(View版本)
    • 4.1.流式布局的实现
      • 4.1.1.测量
      • 4.1.2.布局
    • 4.2针对需求优化流式布局
      • 4.2.1.测量
      • 4.2.2.布局
  • 5.总结

1.前言

最近的版本呢,产品更新了一个直播间的需求,原本直播间的公屏聊天内容基本只展示粉丝等级、会员等级等一两个基本的标签,新的版本呢又加入了很多勋章类型的标签,需要一起展示出来(搞不懂为啥整这么多)! 区别大概就如下图所示(这些等级标签及勋章看着是不是很眼熟):
Snipaste_2023-06-25_16-01-18.png
Snipaste_2023-06-25_16-00-52.png
简单整理了下,大致的区别就是:

  • 旧版的设计只有固定的一两个标签,然后跟上用户发送的文字等信息;
  • 新版的设计要求带不固定数量的标签,少的话可能一两个,多的话标签可能还需要换行,然后跟上用户发送的文字等信息;

那么从旧版到新版需要经历哪些修改呢,一起来复习下自定义View的过程吧。

注: 为了简化处理,所有的标签自定义View都使用图片(ImageView)代替了。

2.旧版设计的分析

先来看下旧版是怎么处理的,旧版UI的蓝图如下:
Snipaste_2023-06-25_16-50-25.png
可以看到,蓝图中标签视图(ImageView)和文本视图(TextView)是重叠在一起的,然而事实也是这样,在旧版的处理中,设置数据后需要手动测量所有标签视图的宽度,测量完毕后让文本缩进所有标签宽度的长度即可。

那么如何做到文本缩进的效果呢?对滴,通过对SpannableString设置相应的Span即可,它支持设置很多类型,常用的如下所示(未列举完全):

  • ForegroundColorSpan

设置文字颜色

  • BackgroudColorSpan

设置文字背景颜色

  • ClickableSpan

设置点击效果

  • URLSpan

设置超链接效果,点击跳转浏览器

  • StrikethroughSpan

设置文字删除线效果

  • UnderlineSpan

设置文字下划线效果

  • ImageSpan

设置文字中插入图片的效果

  • LeadingMarginSpan

设置文字缩进效果

添加文本缩进功能的伪代码则如下所示:

val marginWidth = 1000 // 设置缩进的长度(也就是所有标签测量出来的长度)
val marginSpan = LeadingMarginSpan.Standard(marginWidth, 0)

val spannableString = SpannableString("小青龙")
spannableString.setSpan(marginSpan, 0, 0, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

textView.text = spannableString

3.新版设计的分析

再来看下新版的UI蓝图,根据需求,多个勋章要顺序摆放下来,如果过长还要换行处理,如下所示。相比旧版固定的一两个标签来说,增加了一丢丢难度。
Snipaste_2023-06-25_16-51-03.png
这个时候旧版的功能完全无法满足我们现在的需求了,现在的标签数量不固定,可能没有,可能多到换行,所以我们只能通过自定义布局去搞定了。

具体要怎么做呢,再仔细琢磨下,标签类的控件其实都是流式布局,所有标签按照流式布局顺序摆放即可,需要换行则处理换行,但是最后一个TextView就比较特殊了,如果也按照流式布局处理的话,如下蓝图所示:
Snipaste_2023-06-25_21-05-04.png
当文本长度较短且剩下空间正好够的时候,还是刚好能达到效果的。但是当文本长度过长的时候效果肯定是下层蓝图这样的效果,文本直接新起一行,标签后面一大段的空间就都浪费了。

所以呢,这个时候我们就结合一下旧版的设计,将最后一行的几个标签所占的宽度计算出来,然后给TextView设置一个MarginSpan,然后重新测量其宽度和高度,最后摆放的时候同最后一行标签的顶部和左端对齐布局即可。

4.代码实现(View版本)

分析完毕后我们的思路就大致定下来了,先实现流式布局,针对最后一个TextView需要重新优化再处理。

4.1.流式布局的实现

流式布局的实现,可能对大家都不陌生了,这里简要罗列下几个基础的步骤(去除了margin和padding等其他复杂的逻辑,只留下了主干代码),首先自定义MyFlowLayout继承自ViewGroup。

4.1.1.测量

在onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)方法中,调用measureChild()方法,循环测量子View,并且每次累加当前行子View的宽度lineWidth,记录当前行子View的最高高度lineHeight。因为我们要实现流式布局,所以当下一个子View累计的宽度超出了父容器的宽度时,那么就需要进行换行处理了,如下代码中第19行的注释。此时我们需要记录上一行子View的所占的实际宽度width,以及高度height。

每次测量完一行后,width要取所有行中的最大值lineWidth,height需要累加每行子View中的最高值lineHeight,以此类推,直到所有子View测量完毕,简要代码如下所示:

val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)

var width = 0
var height = 0
var lineWidth = 0
var lineHeight = 0

for (i in 0 until childCount) {
    val child = getChildAt(i)

    measureChild(child, widthMeasureSpec, heightMeasureSpec)

    val childWidth = child.measuredWidth
    val childHeight = child.measuredHeight

    // 换行操作
    if (lineWidth + childWidth > widthSize) {
        height += lineHeight

        lineWidth = childWidth
        lineHeight = childHeight
    } else {
        lineWidth += childWidth
        lineHeight = lineHeight.coerceAtLeast(childHeight)
    }

    width = width.coerceAtLeast(lineWidth)
}

// 加上最后一行的高度
height += lineHeight

setMeasuredDimension(
    if (widthMode == MeasureSpec.EXACTLY) widthSize else width,
    if (heightMode == MeasureSpec.EXACTLY) heightSize else height
)

测量的最后一步是确定父容器的大小,当我们调用 setMeasuredDimension() 方法时,就是在告诉父布局或容器我们自定义布局的实际尺寸。可以将这个方法类比为:一个画家完成绘画后,将画作的实际尺寸告知展览场所,以便场所为画作提供正确的展示空间。

4.1.2.布局

接下来呢就开始布局了,在onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) 函数中,对子View调用layout(int l, int t, int r, int b)函数挨个摆放已经测量好的子View,第一个子View的位置从(left = 0, top = 0, right = 当前子view的宽度, bottom = 当前子View的高度)开始摆放,后续子View的位置就是从上一个位置的末尾开始摆放(left = 累计子View的宽度, top = 0, right = 累计子View的宽度 + 当前子view的宽度, bottom=当前子View的高度)。

以此类推,当摆放下一个子View的宽度超过父容器的宽度时,则进行换行处理,此时left = 0,top = 上一行子View的最大高度,简要代码如下所示:

var childLeft = 0
var childTop = 0

val width = right - left

var lineWidth = 0
var lineHeight = 0


for (i in 0 until childCount) {
    val child = getChildAt(i)

    val childWidth = child.measuredWidth
    val childHeight = child.measuredHeight

    // 换行操作
    if (lineWidth + childWidth > width) {
        childLeft = 0
        childTop += lineHeight

        lineWidth = 0
        lineHeight = childHeight
    }

    val childRight = childLeft + childWidth
    val childBottom = childTop + childHeight

    child.layout(
        childLeft,
        childTop,
        childRight,
        childBottom
    )

    childLeft += childWidth
    lineWidth += childWidth
    lineHeight = lineHeight.coerceAtLeast(childHeight)
}

4.2针对需求优化流式布局

接下来就是在流式布局上的优化过程了,这里我们只针对最后一个子View是TextView的情况,其他暂不考虑,以减少示例的复杂程度,需要实现的蓝图如下所示:
Snipaste_2023-06-27_15-06-49.png

4.2.1.测量

所以上述优化的需求,我们分析后统一的处理方式就是:在测量TextView前,先将最后一行标签的长度测量出来lineWidth,然后给TextView设置一个MarginSpan,长度就是lineWidth,最后再测量这个TextView。

给TextView添加MarginSpan的代码如下所示:

private var marginSpan: LeadingMarginSpan? = null
private fun addMarginSpanToTextView(textView: TextView, lineWidth: Int) {
    
    val oldText = textView.text
    val spannableString = if (oldText is SpannableString) {
        oldText
    } else {
        SpannableString(oldText ?: "")
    }

    // 如果之前有设置过marginSpan的话先清除掉
    if (marginSpan != null) {
        spannableString.removeSpan(marginSpan)
    }

    // 设置新的marginSpan
    marginSpan = LeadingMarginSpan.Standard(lineWidth, 0)
    spannableString.setSpan(marginSpan, 0, 0, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

    textView.text = spannableString
}

设置完MarginSpan后的测量代码简要如下:

// 省略重复代码
...

for (i in 0 until childCount) {
    val child = getChildAt(i)

    // 先添加一个MarginSpan
    if (child is TextView) {
        addMarginSpanToTextView(child, lineWidth)
    }

    measureChild(child, widthMeasureSpec, heightMeasureSpec)

    // 省略重复代码
    ...

    if (child is TextView) {
        lineWidth = childWidth
        lineHeight = childHeight
    } else {
        // 换行操作
        // 省略重复代码
        ....
    }

}

// 省略重复代码
....

4.2.2.布局

布局的时候,前面所有的标签都正常按照流式布局摆放即可,当摆放到最后一个TextView的时候,我们将其直接将其从最后一行标签的起点位置左端对齐,顶部对齐摆放即可,简要代码如下所示:

// 省略重复代码
....

for (i in 0 until childCount) {
    // 省略重复代码
	....

    // 如果是TextView的话直接从头开始摆放
    if (child is TextView) {
        childLeft = paddingLeft
    } else {
        // 换行操作
        // 省略重复代码
		....
    }

    // 省略重复代码
	....
}

5.总结

经过上述步骤之后我们已经实现了一个简单的升级版的流式布局,他支持对最后一个TextView设置MarginSpan的处理,以使得整条公屏的内容更加紧凑。

但是呢,我们还有很多的内容未添加支持,例如margin、padding的处理,子View间横向间距、竖向间距的处理,每行之间子View的对齐方式处理,每列之间子View的对齐方式处理等等,相信剩下的内容难不倒你我,冲啊,去实现它。

最后的最后,View版的自定义效果实现了,Compose版的还远吗?

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

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

相关文章

蓝库云|企业如何建立自己的供应商管理系统?零代码工具带来惊喜

在面对采购的物品和服务比较复杂多样的企业,都需要管理大量的供应商,因此需要更加有效和专业的供应商管理系统。随着数字化转型的意识不断增强,中小型企业也越来越意识到采用供应商系统的必要性,以提高采购效率和质量,…

LangChain 使用文本描述的方式操作MySQL中的数据

一、LangChain 使用文本描述的方式操作MySQL中的数据 在 LangChain 中提供了 SQLDatabaseChain ,可以通过语义文本去操作 MySQL中的数据,例如在 MySQL 中有如下表数据: 用户表 CREATE TABLE user (id int NOT NULL AUTO_INCREMENT COMMENT…

HTML5 Canvas动画实例

在开发在线游戏时,绘制动画是非常重要的。本文介绍一个使用Canvas API实现的动画实例——游戏人物的跑步动画。 01、动画的概念及原理 1、动画 动画是通过一幅幅静止的、内容不同的画面(即帧)快速播放来呈现的,使人们在视觉上产生动的感觉。这是利用了…

如何让VSCode不生成 tempcoderunnerfile.py

原因 runcode插件默认包含执行选中文本功能,当运行程序时会优先执行光标选中的代码并生成temp文件 解决方法 方法一:删除生成文件 在setting.json中添加 "code-runner.executorMap": {"php": "php $fullFileName &&am…

【Linux】Ubuntu20.04使用xrdp远程桌面时,gnome桌面环境没有最小化、任务栏等问题

一、问题背景 如下图所示,终端窗口没有最小化,因此只能关闭或移到一边去。 二、解决办法 在安装任何扩展前,需要将xrdp的桌面尽量向原生桌面靠拢。 在~/.xsessionrc配置文件中添加下面三行。 export GNOME_SHELL_SESSION_MODEubuntu ex…

Mediapipe 人像分割;实时更换背景;人脸添加特效

参考: https://zhuanlan.zhihu.com/p/476351994 1、Mediapipe 人像分割 import cv2 import mediapipe as mp import numpy as np mp_drawing mp.solutions.drawing_utils mp_selfie_segmentation mp.solutions.selfie_segmentation # 图片人物抠图: IMAGE_FILES…

idea生成serialVersionUID序列号

设置idea file->settings,搜索serialVersionUID,勾选框起来的两项 实体类实现Serializable接口 Data public class User implements Serializable { }鼠标放到类名上 点击提示的uid 生成的uid 结束! hy:17 生活是一面镜子,给予我们…

springboot 停车场管理系统-计算机毕设 附源码82061

springboot 停车场管理系统 2023年5月 摘要 由于数据库和数据仓库技术的快速发展,停车场管理系统建设越来越向模块化、智能化、自我服务和管理科学化的方向发展。停车场管理系统对处理对象和服务对象,自身的系统结构,处理能力,都…

GO 基本配置

其他 IDEA 配置go语言环境 https://blog.csdn.net/weixin_45719444/article/details/121726325 关于IDEA的 plugins下找不到GO插件 点击安装 安装插件 Go 安装插件 Generate struct tags for golang

Flink CDC 2.4 正式发布,新增 Vitess 数据源,更多连接器支持增量快照,升级 Debezium 版本...

01 Flink CDC 简介 Flink CDC [1] 是基于数据库的日志 CDC 技术,实现了全增量一体化读取的数据集成框架。配合 Flink 优秀的管道能力和丰富的上下游生态,Flink CDC 可以高效实现海量数据的实时集成。 作为新一代的实时数据集成框架,Flink CDC…

二进制搭建Kubernetes集群(三)——部署多master

本文将完成多master集群的部署,即部署master02,以及nginx负载均衡、keepalived高可用 多master集群架构图: 架构说明: node节点的kubelet只能对接一个master节点的apiserver,不可能同时对接多个master节点的apiserver…

【Solr】中文分词配置

提示:在设置中文分词前需确保已经生成过core,未生成core的可以使用:solr create -c "自定义名称"进行定义。 未分词前的效果预览: 下载分词器: 下载地址: https://mvnrepository.com/artifact/com.github.m…

山西电力市场日前价格预测【2023-06-29】

日前价格预测 预测明日(2023-06-29)山西电力市场全天平均日前电价为407.26元/MWh。其中,最高日前价格为539.37元/MWh,预计出现在21: 15。最低日前电价为312.43元/MWh,预计出现在13: 00。以上预测仅供学习参考&#xff…

Java调用ssl异常,javax.net.ssl.SSLHandshakeException: No appropriate protocol

现象:sqlserver 2017 安装在docker里,系统是mac 13,java 1.8.371运行java程序提示上面ssl错误,根据百度提供的方法,修改文件,重启程序搞定。 解决办法:java.security 找到这个文件修改保存 发…

RPC(Remote Procedure Call)学习

目录 一、概念 二、RPC 调用基本流程 一、概念 RPC 全称是 Remote Procedure Call (远程过程调用),它是一种通过网络从远程计算机程序上请求服务,可以提供终结点映射程序以及RPC服务,而不需要了解底层网络技术的协议…

三位数字显示电容测试表

广大电子爱好者都有这样的体会,中、高档数字万用表虽有电容测试挡位,但测量范围一般仅为 1pF~20F,往往不能满足使用者的需要,给电容测量带来不便。本电路介绍的三位数显示电容测试表采用四块集成电路,电路简洁、容易制…

element-ui中el-table设置固定高度后,底部合计栏被遮盖

如图: 解决办法:el-table加上ref"summaryTab",然后在自定义合计计算方法getSummaries中加上如下代码就ok了 this.$nextTick(() > {this.$refs.summaryTab.doLayout(); }); 没用使用自定义合计计算函数的,也可以写在updated中,如下: updated() {this.$nextTick((…

关于ipad:无法验证服务器身份

ipad 连接网络后,有时候会冒出这个弹窗,并且关掉后仍继续弹出 可以尝试以下几种方法:(我是用③解决的) ①. 确保你的iPad连接的是稳定的网络。有时候网络连接不稳定会导致无法验证服务器身份。 我们学校这个校园网…

Karl Guttag评Vision Pro(三):为什么飞机上VR观影体验不佳?

在过去25年里,AR/VR头显显示技术得到长足发展,采用的屏幕规格越来越高。据早前报告预测,VR头显安装基数会在2023年达到约2500万台。尽管如此,相比于无处不在的手机,我们几乎看不到周围有人随身携带AR/VR头显&#xff0…

APP为什么没有被小程序取代呢?

在科技行业日新月异的发展下,一直存在一种声音,认为小程序将逐步取代APP。然而,事实却并非如此。APP至今仍然以其独特的优势,坚挺地存在于市场之中。这是为何呢?以下是我们对此的几点考察。 首先,APP在功能…