热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:Android技术分享|自定义ViewGroup实现直播间大小屏无缝切换

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Android技术分享|自定义ViewGroup实现直播间大小屏无缝切换相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Android技术分享| 自定义ViewGroup实现直播间大小屏无缝切换相关的知识,希望对你有一定的参考价值。




源代码地址:请点击这里


需求

两种显示方式:



  1. 主播全屏,其他游客悬浮在右侧。下面简称大小屏模式。



  1. 所有人等分屏幕。下面简称等分模式。


分析



  • 最多4人连麦,明确这点方便定制坐标算法。

  • 自定义的 ViewGroup 最好分别提供等分模式和大小屏模式的边距设置接口,便于修改。

  • SDK 自己管理了 TextureView 的绘制和测量,所以 ViewGroup 需要复写 onMeasure 方法以通知 TextureView 测量和绘制。

  • 一个计算 0.0f ~ 1.0f 逐渐减速的函数,给动画过程做支撑。

  • 一个记录坐标的数据模型。和一个根据现有 Child View 的数量计算两种布局模式下,每个 View 摆放位置的函数。


实现

1.定义坐标数据模型

private data class ViewLayoutInfo(
var originalLeft: Int = 0,// original开头的为动画开始前的起始值
var originalTop: Int = 0,
var originalRight: Int = 0,
var originalBottom: Int = 0,
var left: Float = 0.0f,// 无前缀的为动画过程中的临时值
var top: Float = 0.0f,
var right: Float = 0.0f,
var bottom: Float = 0.0f,
var toLeft: Int = 0,// to开头的为动画目标值
var toTop: Int = 0,
var toRight: Int = 0,
var toBottom: Int = 0,
var progress: Float = 0.0f,// 进度 0.0f ~ 1.0f,用于控制 Alpha 动画
var isAlpha: Boolean = false,// 透明动画,新添加的执行此动画
var isConverted: Boolean = false,// 控制 progress 反转的标记
var waitingDestroy: Boolean = false,// 结束后销毁 View 的标记
var pos: Int = 0// 记录自己索引,以便销毁
) {
init {
left = originalLeft.toFloat()
top = originalTop.toFloat()
right = originalRight.toFloat()
bottom = originalBottom.toFloat()
}
}

以上,记录了执行动画和销毁View所需的数据。(于源码中第352行)

2.计算不同展示模式下View坐标的函数

if (layoutTopicMode) {
var index = 0
for (i in 1 until childCount) if (i != position) (getChildAt(i).tag as ViewLayoutInfo).run {
toLeft = measuredWidth - maxWidgetPadding - smallViewWidth
toTop = defMultipleVideosTopPadding + index * smallViewHeight + index * maxWidgetPadding
toRight = measuredWidth - maxWidgetPadding
toBottom = toTop + smallViewHeight
index++
}
} else {
var posOffset = 0
var pos = 0
if (childCount == 4) {
posOffset = 2
pos++
(getChildAt(0).tag as ViewLayoutInfo).run {
toLeft = measuredWidth.shr(1) - multiViewWidth.shr(1)
toTop = defMultipleVideosTopPadding
toRight = measuredWidth.shr(1) + multiViewWidth.shr(1)
toBottom = defMultipleVideosTopPadding + multiViewHeight
}
}
for (i in pos until childCount) if (i != position) {
val topFloor = posOffset / 2
val leftFloor = posOffset % 2
(getChildAt(i).tag as ViewLayoutInfo).run {
toLeft = leftFloor * measuredWidth.shr(1) + leftFloor * multipleWidgetPadding
toTop = topFloor * multiViewHeight + topFloor * multipleWidgetPadding + defMultipleVideosTopPadding
toRight = toLeft + multiViewWidth
toBottom = toTop + multiViewHeight
}
posOffset++
}
}
post(AnimThread(
(0 until childCount).map { getChildAt(it).tag as ViewLayoutInfo }.toTypedArray()
))

Demo源码中的add、remove、toggle方法重复代码过多,未来得及优化。这里只附上 addVideoView 中的计算部分(于源代码中第141行),只需稍微修改即可适用add、remove和toggle。(也可参考 CDNLiveVM 中的 calcPosition 方法,为经过优化的版本)layoutTopicMode = true 时,为大小屏模式。

由于是定制算法,只能适用这一种布局,故不写注释。只需明确一点,此方法最终目的是为了计算出每个View当前应该出现的位置,保存到上面定义的数据模型中并开启动画(最后一行 post AnimThread 为开启动画的代码,我这里是通过 post 一个线程来更新每一帧)。

可根据不同的需求写不同的实现,最终符合定义的数据模型即可。

3.逐渐减速的算法,使动画效果看起来更自然。

private inner class AnimThread(
private val viewInfoList: Array,
private var duration: Float = 180.0f,
private var processing: Float = 0.0f
) : Runnable {
private val waitingTime = 9L
override fun run() {
var progress = processing / duration
if (progress > 1.0f) {
progress = 1.0f
}
for (viewInfo in viewInfoList) {
if (viewInfo.isAlpha) {
viewInfo.progress = progress
} else viewInfo.run {
val diffLeft = (toLeft - originalLeft) * progress
val diffTop = (toTop - originalTop) * progress
val diffRight = (toRight - originalRight) * progress
val diffBottom = (toBottom - originalBottom) * progress
left = originalLeft + diffLeft
top = originalTop + diffTop
right = originalRight + diffRight
bottom = originalBottom + diffBottom
}
}
requestLayout()
if (progress <1.0f) {
if (progress > 0.8f) {
var offset = ((progress - 0.7f) / 0.25f)
if (offset > 1.0f)
offset = 1.0f
processing += waitingTime - waitingTime * progress * 0.95f * offset
} else {
processing += waitingTime
}
postDelayed(this@AnimThread, waitingTime)
} else {
for (viewInfo in viewInfoList) {
if (viewInfo.waitingDestroy) {
removeViewAt(viewInfo.pos)
} else viewInfo.run {
processing = 0.0f
duration = 0.0f
originalLeft = left.toInt()
originalTop = top.toInt()
originalRight = right.toInt()
originalBottom = bottom.toInt()
isAlpha = false
isCOnverted= false
}
}
animRunning = false
processing = duration
if (!taskLink.isEmpty()) {
invokeLinkedTask()// 此方法执行正在等待中的任务,从源码中能看到,remove、add等函数需要依次执行,前一个动画未执行完毕就进行下一个动画可能会导致不可预知的错误。
}
}
}
}

上述代码除了提供减速算法,还一并更新了对应View数据模型的中间值,也就是模型定义种的 left, top, right, bottom 。

通过减速算法提供的进度值,乘以目标坐标与起始坐标的间距,得出中间值。

逐渐减速的算法关键代码为:

if (progress > 0.8f) {
var offset = ((progress - 0.7f) / 0.25f)
if (offset > 1.0f)
offset = 1.0f
processing += waitingTime - waitingTime * progress * 0.95f * offset
} else {
processing += waitingTime
}

这个算法实现的有缺陷,因为它直接修改了进度时间,大概率会导致执行完毕的时间与设置的预期时间(如设置200ms执行完毕,实际可能超过200ms)不符。文末我会提供一个优化的减速算法。

变量 waitingTime 表示等待多久执行下一帧动画。用每秒1000ms计算即可,如果目标为60刷新率的动画,设置为1000 / 60 = 16.66667即可(近似值)。

计算并存储每个 View 的中间值后,调用 requestLayout() 通知系统的 onMeasure 和 onLayout 方法,重新摆放 View 。

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if (childCount == 0)
return
for (i in 0 until childCount) {
val child = getChildAt(i)
val layoutInfo = child.tag as ViewLayoutInfo
child.layout(
layoutInfo.left.toInt(),
layoutInfo.top.toInt(),
layoutInfo.right.toInt(),
layoutInfo.bottom.toInt()
)
if (layoutInfo.isAlpha) {
val progress = if (layoutInfo.isConverted)
1.0f - layoutInfo.progress
else
layoutInfo.progress
child.alpha = progress
}
}
}

4.定义边距相关的变量,供简单的定制修改

/**
* @param multipleWidgetPadding : 等分模式读取
* @param maxWidgetPadding : 大小屏布局读取
* @param defMultipleVideosTopPadding : 距离顶部变距
*/
private var multipleWidgetPadding = 0
private var maxWidgetPadding = 0
private var defMultipleVideosTopPadding = 0
init {
viewTreeObserver.addOnGlobalLayoutListener(this)
attrs?.let {
val typedArray = resources.obtainAttributes(it, R.styleable.AnyVideoGroup)
multipleWidgetPadding = typedArray.getDimensionPixelOffset(
R.styleable.AnyVideoGroup_between23viewsPadding, 0
)
maxWidgetPadding = typedArray.getDimensionPixelOffset(
R.styleable.AnyVideoGroup_at4smallViewsPadding, 0
)
defMultipleVideosTopPadding = typedArray.getDimensionPixelOffset(
R.styleable.AnyVideoGroup_defMultipleVideosTopPadding, 0
)
layoutTopicMode = typedArray.getBoolean(
R.styleable.AnyVideoGroup_initTopicMode, layoutTopicMode
)
typedArray.recycle()
}
}

取名时对这三个变量的职责定义,与编写逻辑时的定义有出入,所以有点词不达意,需参考注释。

由于这只是定制化的变量,并不重要,可根据业务逻辑自行随意修改。

5.复写 onMeasure 方法,这里主要是通知 TextureView 更新大小。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
multiViewWidth = widthSize.shr(1)
multiViewHeight = (multiViewWidth.toFloat() * 1.33334f).toInt()
smallViewWidth = (widthSize * 0.3125f).toInt()
smallViewHeight = (smallViewWidth.toFloat() * 1.33334f).toInt()
for (i in 0 until childCount) {
val child = getChildAt(i)
val info = child.tag as ViewLayoutInfo
child.measure(
MeasureSpec.makeMeasureSpec((info.right - info.left).toInt(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec((info.bottom - info.top).toInt(), MeasureSpec.EXACTLY)
)
}
setMeasuredDimension(
MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY)
)
}

总结



  1. 明确数据模型,一般情况下记录起始上下左右坐标、目标上下左右坐标、和进度百分比就足够了。

  2. 根据需求明确动画算法,这里补充一下优化的减速算法:

factor = 1.0
if (factor == 1.0)
(1.0 - (1.0 - x) * (1.0 - x))
else
(1.0 - pow((1.0 - x), 2 * factor))
// x = time.


  1. 根据算法计算出来的值更新 layout 布局即可。

此类 ViewGroup 实现简单方便,只涉及到几个基本系统API。如不想写 onMeasure 方法可继承 FrameLayout 等已写好 onMeasure 实现的 ViewGroup 。


推荐阅读
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • 本文介绍了在Linux下安装Perl的步骤,并提供了一个简单的Perl程序示例。同时,还展示了运行该程序的结果。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
author-avatar
mobiledu2502860983
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有