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

源代码地址:(
https://github.com/anyRTC-UseCase/VideoLive
 

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

文章插图
 
需求两种显示方式:
  1. 主播全屏,其他游客悬浮在右侧 。下面简称大小屏模式 。
  2. 所有人等分屏幕 。下面简称等分模式 。
 
Android技术分享|自定义ViewGroup实现直播间大小屏无缝切换

文章插图
 

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

文章插图
 
分析
  • 最多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 = 0for (i in 1 until childCount) if (i != position) (getChildAt(i).tag as ViewLayoutInfo).run {toLeft = measuredWidth - maxWidgetPadding - smallViewWidthtoTop = defMultipleVideosTopPadding + index * smallViewHeight + index * maxWidgetPaddingtoRight = measuredWidth - maxWidgetPaddingtoBottom = toTop + smallViewHeightindex++}} else {var posOffset = 0var pos = 0if (childCount == 4) {posOffset = 2pos++(getChildAt(0).tag as ViewLayoutInfo).run {toLeft = measuredWidth.shr(1) - multiViewWidth.shr(1)toTop = defMultipleVideosTopPaddingtoRight = measuredWidth.shr(1) + multiViewWidth.shr(1)toBottom = defMultipleVideosTopPadding + multiViewHeight}}for (i in pos until childCount) if (i != position) {val topFloor = posOffset / 2val leftFloor = posOffset % 2(getChildAt(i).tag as ViewLayoutInfo).run {toLeft = leftFloor * measuredWidth.shr(1) + leftFloor * multipleWidgetPaddingtoTop = topFloor * multiViewHeight + topFloor * multipleWidgetPadding + defMultipleVideosTopPaddingtoRight = toLeft + multiViewWidthtoBottom = 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<ViewLayoutInfo>,private var duration: Float = 180.0f,private var processing: Float = 0.0f) : Runnable {private val waitingTime = 9Loverride fun run() {var progress = processing / durationif (progress > 1.0f) {progress = 1.0f}for (viewInfo in viewInfoList) {if (viewInfo.isAlpha) {viewInfo.progress = progress} else viewInfo.run {val diffLeft = (toLeft - originalLeft) * progressval diffTop = (toTop - originalTop) * progressval diffRight = (toRight - originalRight) * progressval diffBottom = (toBottom - originalBottom) * progressleft = originalLeft + diffLefttop = originalTop + diffTopright = originalRight + diffRightbottom = originalBottom + diffBottom}}requestLayout()if (progress < 1.0f) {if (progress > 0.8f) {var offset = ((progress - 0.7f) / 0.25f)if (offset > 1.0f)offset = 1.0fprocessing += 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.0fduration = 0.0foriginalLeft = left.toInt()originalTop = top.toInt()originalRight = right.toInt()originalBottom = bottom.toInt()isAlpha = falseisConverted = false}}animRunning = falseprocessing = durationif (!taskLink.isEmpty()) {invokeLinkedTask()// 此方法执行正在等待中的任务,从源码中能看到,remove、add等函数需要依次执行,前一个动画未执行完毕就进行下一个动画可能会导致不可预知的错误 。}}}}


推荐阅读