草庐IT

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

anyRTC 2023-03-28 原文
源代码地址:请点击这里

需求

两种显示方式:

  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<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.
  1. 根据算法计算出来的值更新 layout 布局即可。
此类 ViewGroup 实现简单方便,只涉及到几个基本系统API。如不想写 onMeasure 方法可继承 FrameLayout 等已写好 onMeasure 实现的 ViewGroup 。

有关Android技术分享| 自定义ViewGroup实现直播间大小屏无缝切换的更多相关文章

  1. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

    我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

  2. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  3. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  4. ruby - 主要 :Object when running build from sublime 的未定义方法 `require_relative' - 2

    我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby​​1.9+ 关于ruby-主要:Objectwhenrun

  5. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

  6. ruby - 定义方法参数的条件 - 2

    我有一个只接受一个参数的方法:defmy_method(number)end如果使用number调用方法,我该如何引发错误??通常,我如何定义方法参数的条件?比如我想在调用的时候报错:my_method(1) 最佳答案 您可以添加guard在函数的开头,如果参数无效则引发异常。例如:defmy_method(number)failArgumentError,"Inputshouldbegreaterthanorequalto2"ifnumbereputse.messageend#=>Inputshouldbegreaterthano

  7. ruby - 如何在 Grape 中定义哈希数组? - 2

    我使用Ember作为我的前端和GrapeAPI来为我的API提供服务。前端发送类似:{"service"=>{"name"=>"Name","duration"=>"30","user"=>nil,"organization"=>"org","category"=>nil,"description"=>"description","disabled"=>true,"color"=>nil,"availabilities"=>[{"day"=>"Saturday","enabled"=>false,"timeSlots"=>[{"startAt"=>"09:00AM","endAt"=>

  8. ruby - 获取模块中定义的所有常量的值 - 2

    我想获取模块中定义的所有常量的值:moduleLettersA='apple'.freezeB='boy'.freezeendconstants给了我常量的名字:Letters.constants(false)#=>[:A,:B]如何获取它们的值的数组,即["apple","boy"]? 最佳答案 为了做到这一点,请使用mapLetters.constants(false).map&Letters.method(:const_get)这将返回["a","b"]第二种方式:Letters.constants(false).map{|c

  9. ruby - 这两个 Ruby 类初始化定义有什么区别? - 2

    我正在阅读一本关于Ruby的书,作者在编写类初始化定义时使用的形式与他在本书前几节中使用的形式略有不同。它看起来像这样:classTicketattr_accessor:venue,:datedefinitialize(venue,date)self.venue=venueself.date=dateendend在本书的前几节中,它的定义如下:classTicketattr_accessor:venue,:datedefinitialize(venue,date)@venue=venue@date=dateendend在第一个示例中使用setter方法与在第二个示例中使用实例变量之间是

  10. ruby-on-rails - 如何生成传递一些自定义参数的 `link_to` URL? - 2

    我正在使用RubyonRails3.0.9,我想生成一个传递一些自定义参数的link_toURL。也就是说,有一个articles_path(www.my_web_site_name.com/articles)我想生成如下内容:link_to'Samplelinktitle',...#HereIshouldimplementthecode#=>'http://www.my_web_site_name.com/articles?param1=value1¶m2=value2&...我如何编写link_to语句“alàRubyonRailsWay”以实现该目的?如果我想通过传递一些

随机推荐