赞
踩
源代码地址:请点击这里
两种显示方式:
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) ) }
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.
此类 ViewGroup 实现简单方便,只涉及到几个基本系统API。如不想写 onMeasure 方法可继承 FrameLayout 等已写好 onMeasure 实现的 ViewGroup 。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。