源代碼地址:(
https://github.com/anyRTC-UseCase/VideoLive)
需求
兩種顯示方式:
- 主播全屏,其他游客懸浮在右側(cè)。下面簡(jiǎn)稱(chēng)大小屏模式。
- 所有人等分屏幕。下面簡(jiǎn)稱(chēng)等分模式。
分析
- 最多4人連麥,明確這點(diǎn)方便定制坐標(biāo)算法。
- 自定義的 ViewGroup 最好分別提供等分模式和大小屏模式的邊距設(shè)置接口,便于修改。
- SDK 自己管理了 TextureView 的繪制和測(cè)量,所以 ViewGroup 需要復(fù)寫(xiě) onMeasure 方法以通知 TextureView 測(cè)量和繪制。
- 一個(gè)計(jì)算 0.0f ~ 1.0f 逐漸減速的函數(shù),給動(dòng)畫(huà)過(guò)程做支撐。
- 一個(gè)記錄坐標(biāo)的數(shù)據(jù)模型。和一個(gè)根據(jù)現(xiàn)有 Child View 的數(shù)量計(jì)算兩種布局模式下,每個(gè) View 擺放位置的函數(shù)。
實(shí)現(xiàn)
1.定義坐標(biāo)數(shù)據(jù)模型
private data class ViewLayoutInfo(
var originalLeft: Int = 0,// original開(kāi)頭的為動(dòng)畫(huà)開(kāi)始前的起始值
var originalTop: Int = 0,
var originalRight: Int = 0,
var originalBottom: Int = 0,
var left: Float = 0.0f,// 無(wú)前綴的為動(dòng)畫(huà)過(guò)程中的臨時(shí)值
var top: Float = 0.0f,
var right: Float = 0.0f,
var bottom: Float = 0.0f,
var toLeft: Int = 0,// to開(kāi)頭的為動(dòng)畫(huà)目標(biāo)值
var toTop: Int = 0,
var toRight: Int = 0,
var toBottom: Int = 0,
var progress: Float = 0.0f,// 進(jìn)度 0.0f ~ 1.0f,用于控制 Alpha 動(dòng)畫(huà)
var isAlpha: Boolean = false,// 透明動(dòng)畫(huà),新添加的執(zhí)行此動(dòng)畫(huà)
var isConverted: Boolean = false,// 控制 progress 反轉(zhuǎn)的標(biāo)記
var waitingDestroy: Boolean = false,// 結(jié)束后銷(xiāo)毀 View 的標(biāo)記
var pos: Int = 0// 記錄自己索引,以便銷(xiāo)毀
) {
init {
left = originalLeft.toFloat()
top = originalTop.toFloat()
right = originalRight.toFloat()
bottom = originalBottom.toFloat()
}
}
以上,記錄了執(zhí)行動(dòng)畫(huà)和銷(xiāo)毀View所需的數(shù)據(jù)。(于源碼中第352行)
2.計(jì)算不同展示模式下View坐標(biāo)的函數(shù)
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方法重復(fù)代碼過(guò)多,未來(lái)得及優(yōu)化。這里只附上 addVideoView 中的計(jì)算部分(于源代碼中第141行),只需稍微修改即可適用add、remove和toggle。(也可參考 CDNLiveVM 中的 calcPosition 方法,為經(jīng)過(guò)優(yōu)化的版本)layoutTopicMode = true 時(shí),為大小屏模式。
由于是定制算法,只能適用這一種布局,故不寫(xiě)注釋。只需明確一點(diǎn),此方法最終目的是為了計(jì)算出每個(gè)View當(dāng)前應(yīng)該出現(xiàn)的位置,保存到上面定義的數(shù)據(jù)模型中并開(kāi)啟動(dòng)畫(huà)(最后一行 post AnimThread 為開(kāi)啟動(dòng)畫(huà)的代碼,我這里是通過(guò) post 一個(gè)線(xiàn)程來(lái)更新每一幀)。
可根據(jù)不同的需求寫(xiě)不同的實(shí)現(xiàn),最終符合定義的數(shù)據(jù)模型即可。
3.逐漸減速的算法,使動(dòng)畫(huà)效果看起來(lái)更自然。
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()// 此方法執(zhí)行正在等待中的任務(wù),從源碼中能看到,remove、add等函數(shù)需要依次執(zhí)行,前一個(gè)動(dòng)畫(huà)未執(zhí)行完畢就進(jìn)行下一個(gè)動(dòng)畫(huà)可能會(huì)導(dǎo)致不可預(yù)知的錯(cuò)誤。
}
}
}
}
上述代碼除了提供減速算法,還一并更新了對(duì)應(yīng)View數(shù)據(jù)模型的中間值,也就是模型定義種的 left, top, right, bottom 。
通過(guò)減速算法提供的進(jìn)度值,乘以目標(biāo)坐標(biāo)與起始坐標(biāo)的間距,得出中間值。
逐漸減速的算法關(guān)鍵代碼為:
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
}
這個(gè)算法實(shí)現(xiàn)的有缺陷,因?yàn)樗苯有薷牧诉M(jìn)度時(shí)間,大概率會(huì)導(dǎo)致執(zhí)行完畢的時(shí)間與設(shè)置的預(yù)期時(shí)間(如設(shè)置200ms執(zhí)行完畢,實(shí)際可能超過(guò)200ms)不符。文末我會(huì)提供一個(gè)優(yōu)化的減速算法。
變量 waitingTime 表示等待多久執(zhí)行下一幀動(dòng)畫(huà)。用每秒1000ms計(jì)算即可,如果目標(biāo)為60刷新率的動(dòng)畫(huà),設(shè)置為1000 / 60 = 16.66667即可(近似值)。
計(jì)算并存儲(chǔ)每個(gè) View 的中間值后,調(diào)用 requestLayout() 通知系統(tǒng)的 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.定義邊距相關(guān)的變量,供簡(jiǎn)單的定制修改
/**
* @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()
}
}
取名時(shí)對(duì)這三個(gè)變量的職責(zé)定義,與編寫(xiě)邏輯時(shí)的定義有出入,所以有點(diǎn)詞不達(dá)意,需參考注釋。
由于這只是定制化的變量,并不重要,可根據(jù)業(yè)務(wù)邏輯自行隨意修改。
5.復(fù)寫(xiě) 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)
)
}
總結(jié)
1.明確數(shù)據(jù)模型,一般情況下記錄起始上下左右坐標(biāo)、目標(biāo)上下左右坐標(biāo)、和進(jìn)度百分比就足夠了。
2.根據(jù)需求明確動(dòng)畫(huà)算法,這里補(bǔ)充一下優(yōu)化的減速算法:
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.根據(jù)算法計(jì)算出來(lái)的值更新 layout 布局即可。
此類(lèi) ViewGroup 實(shí)現(xiàn)簡(jiǎn)單方便,只涉及到幾個(gè)基本系統(tǒng)API。如不想寫(xiě) onMeasure 方法可繼承 FrameLayout 等已寫(xiě)好 onMeasure 實(shí)現(xiàn)的 ViewGroup 。






