您当前的位置:首页 > 电脑百科 > 程序开发 > 移动端 > Android

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

时间:2021-08-18 10:34:38  来源:  作者:anyRTC云平台

源代码地址:(
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 = 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<ViewLayoutInfo>,
    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.

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

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



Tags:Android技术   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
源代码地址:( https://github.com/anyRTC-UseCase/VideoLive) 需求两种显示方式: 主播全屏,其他游客悬浮在右侧。下面简称大小屏模式。 所有人等分屏幕。下面简称等分模式。 ...【详细内容】
2021-08-18  Tags: Android技术  点击:(66)  评论:(0)  加入收藏
以下文章来源于Android达摩院 ,作者Gityuan 引言众所周知,Android是谷歌开发的一款基于Linux的开源操作系统,每年迭代一次大版本升级。小米、华为、OPPO、VIVO、三星等各大厂商...【详细内容】
2020-07-21  Tags: Android技术  点击:(79)  评论:(0)  加入收藏
前言越来越多的人在提“移动端的下半场”、“Android开发的焦虑”之类的,也有人在喊“技术天天在变,学也学不完”,“昨天Kotlin今天Flutter”。其实我却认为,如果你技术达到了一...【详细内容】
2020-07-13  Tags: Android技术  点击:(78)  评论:(0)  加入收藏
▌简易百科推荐
今天面试遇到同学说做过内存优化,于是我一般都会问那 Bitmap 的像素内存存在哪?大多数同学都回答在 java heap 里面,就比较尴尬,理论上你做内存优化,如果连图片这个内存大户内存...【详细内容】
2021-12-23  像程序那样思考    Tags:Android开发   点击:(6)  评论:(0)  加入收藏
Android logcat日志封装logcat痛点在Android开发中使用logcat非常频繁,logcat能帮我们定位问题,但是在日常使用中发现每次使用都需要传递tag,并且会遇到输出频率很高的log,在多...【详细内容】
2021-12-22  YuCoding    Tags:Android   点击:(7)  评论:(0)  加入收藏
对项目的基本介绍 1.整个框架主要是给MVVM框架使用的,自己写完interface接口后,通过自定义的注解就能自动生成接口方法 2.用Kotlin的Flow去代替Rxjava,因为我发现RxJava功能很...【详细内容】
2021-12-08  网易Leo    Tags:Android开发   点击:(16)  评论:(0)  加入收藏
前言在Android开发过程中,有些时候会根据需要引用别的项目到当前项目里面,而且以Module形式引用。所以本篇博文就来分享一下怎么以Module形式引用别的项目到当前项目中,方便开...【详细内容】
2021-12-07  网易Leo    Tags:Android开发   点击:(21)  评论:(0)  加入收藏
作者:fundroid这篇文章偏阅读一些,大家可以了解下 Android 的一些最新动向。每年9/10月份 Google 都会举行约为期2天的 Android Dev Summit,在活动上 Google 的技术专家们会分...【详细内容】
2021-11-30  像程序那样思考    Tags:Android开发   点击:(15)  评论:(0)  加入收藏
一、 准备工作1、安装JDK,下载地址(可能需要一个oracle账号,大家百度一下或者自行注册一个就行。尽可能选择8或者11,这两个是长期版本)Java SE | Oracle Technology Network | Or...【详细内容】
2021-11-23  永沧    Tags:Android   点击:(26)  评论:(0)  加入收藏
使用Maven Publish Plugin插件。(官方支持)一、在Library的build.gradle中配置plugins { id &#39;com.android.library&#39; id &#39;kotlin-android&#39; id &#39;k...【详细内容】
2021-11-05  羊城小阳    Tags:Android   点击:(36)  评论:(0)  加入收藏
谷歌离推出Play Store应用程序的新数据隐私部分又近了一步。应用程序开发人员现在可以通过谷歌在Play控制台的新 "数据安全表 "填写相关细节。该公司表示,所需信息将从2022年...【详细内容】
2021-10-20    中关村在线  Tags:安卓   点击:(57)  评论:(0)  加入收藏
架构究竟是什么?如何更好的理解架构?我们知道一个APP通常是由class组成,而这些class之间如何组合,相互之间又如何产生作用,就是影响这个APP的关键点。细分的话我们可以将其分为类...【详细内容】
2021-09-17  像程序那样思考    Tags:Android架构   点击:(51)  评论:(0)  加入收藏
概述当Android应用程序需要访问设备上的敏感资源时,应用程序开发人员会使用权限模型。虽然该模型使用起来非常简单,但开发人员在使用权限时容易出错,从而导致安全漏洞。本文中,...【详细内容】
2021-09-07  SecTr安全团队    Tags:Android开发   点击:(66)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条