在之前的文章中我們實作了自定義View需要的基本功能,本篇中我們通過 Timer 實作影片功能,我偷偷修改了一些資料結構,一會在下面貼出來,
最終效果圖:

影片是通過 Timer 每17毫秒呼叫 View#post 來呼叫主執行緒更新一幀,定義一個 interpolator 使影片效果更自然(逐漸減速的效果),
首先定義一個存盤執行影片相關的資料結構:
private data class AnimInfo(
val block: (percentage: Float) -> Unit,// 每幀呼叫
val duration: Long = 510,
val progress: Long = 0L,
val done: () -> Unit = {}// 影片結束時呼叫
)
還有修改過的存盤訊息相關的資料結構:
data class Message(
val avatar: String,// 頭像
val nickname: String,// 昵稱
val joinRoom: Int = 1,// 1=加入,其他為退出
var shader: BitmapShader? = null,
var bitmap: Bitmap? = null,
var life: Long = 0L,// 當前時間
val lifeTime: Long = 5000L, // 最大存在時間
)
使用鏈表來存盤 Message 和 AnimInfo 資料:
private val animArr = LinkedList<AnimInfo>()
private val dataArr = LinkedList<Message>()
使用一個 Timer 計時影片及更新 Message 的已存在時間,在 init 方法中初始化:
init {
paint.textSize = fontSize.toFloat()
paint.style = Paint.Style.FILL
val metrics = paint.fontMetrics
fontCenterOffset = (abs(metrics.top) - metrics.bottom) / 2f
timer = Timer()
timer.schedule(object : TimerTask() {
override fun run() {
if (dataArr.isNotEmpty()) {// 存在時間計時
dataArr.forEach {
it.life += 17L
}
val first = dataArr.first
if (first.life >= first.lifeTime) {// 當第一條超過最高存在時間時移除
dismissFirstMessage(true)
}
}
if (animArr.isEmpty()) {// 未注冊任何影片則直接跳過
return
}
val i = animArr.iterator()// 序列化移除較為方便
while (i.hasNext()) {
val next = i.next()
next.progress += 17L
var percentage = next.progress.toFloat() / next.duration
if (percentage > 1.0f)
percentage = 1.0f
post { next.block.invoke(interpolator(percentage)) }// 每幀呼叫
if (next.progress >= next.duration) {// 影片執行完畢則呼叫 done 并移除自己
post { next.done.invoke() }
i.remove()
}
}
}
}, 0, 17)
}
interpolator 的實作:
private fun interpolator(x: Float): Float = (1.0f - (1.0f - x) * (1.0f - x))
記得在 onDetachedFromWindow 中將 timer 任務注銷:
override fun onDetachedFromWindow() {
timer.cancel()
timer.purge()
super.onDetachedFromWindow()
}
定義 registerAnimator 方法,使開啟一個影片更有儀式感(不是
private fun registerAnimator(animInfo: AnimInfo) {
animArr.add(animInfo)
}
洗掉了 drawMessage 方法,添加了 addMessage 方法和 removeFirstMessage 方法,
addMessage 方法:
fun addMessage(msg: Message) {
if (!this::mBufferBitmap.isInitialized) {// 尚未初始化完成通過 post 等待初始化
post { addMessage(msg) }
return
}
// 影片執行中或目前展示的通知數量已達上限則添加到 waitList 中,等待執行
if (animRunning || dataArr.size == limitMessageSize) {
if (dataArr.size == limitMessageSize)
dismissFirstMessage()
waitList.add(msg)
return
}
animRunning = true
dataArr.add(msg)
val nicknameWidth = paint.measureText(msg.nickname)
val msgWidth = nicknameWidth + basedMessageWidth
loadImage(msg.avatar) { bitmap, b ->
if (!b) return@loadImage
val shader = BitmapShader(bitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
msg.let {
it.bitmap = bitmap
it.shader = shader
}
}
// 這里只更新新增的那一條,所以不需要清空之前繪制好的資料
val yOffset = (dataArr.size - 1) * (messageHeight + messagePadding).toFloat()
registerAnimator(AnimInfo({ percentage ->
val xOffset = msgWidth + -(percentage * msgWidth)
mBufferMatrix.reset()
mBufferMatrix.postTranslate(xOffset, yOffset)
mBufferCanvas.setMatrix(mBufferMatrix)
drawMsg(msg, msgWidth, nicknameWidth)
invalidate()
}) {
// 影片結束后先判斷是否有等待洗掉的任務,再判斷是否有等待添加的任務
animRunning = false
if (waitingRemove > 0) {
removeFirstMessage()
} else if (waitList.isNotEmpty()) {
addMessage(waitList.removeFirst())
}
})
}
removeFirstMessage 方法:
// timer 每17毫秒輪詢一次,如果影片在執行中會導致 waitingRemove 增加非常多
// 所以 timer 傳進來的不增加等待洗掉次數
fun removeFirstMessage(isFromTimer: Boolean = false) {
if (dataArr.isEmpty())
return
if (animRunning) {
if (!isFromTimer) waitingRemove++
return
}
animRunning = true
registerAnimator(AnimInfo({ percentage ->
// 因為改動兩條資料并且是上下平移,需要清空上次繪制內容
mBufferBitmap.eraseColor(Color.TRANSPARENT)
for (i in 0 until dataArr.size) {
val item = dataArr[i]
val nicknameWidth = paint.measureText(item.nickname)
val msgWidth = nicknameWidth + basedMessageWidth
val msgHeight = (messageHeight + messagePadding).toFloat()
mBufferMatrix.reset()
mBufferMatrix.setTranslate(0f, (i * msgHeight) - (percentage * msgHeight))
mBufferCanvas.setMatrix(mBufferMatrix)
drawMsg(item, msgWidth, nicknameWidth)
invalidate()
}
}) {
animRunning = false
dataArr.removeFirst().bitmap?.recycle()
if (waitList.size > 0) {// 先判斷是否有等待添加的訊息,與 addMessage 剛好相反
addMessage(waitList.removeFirst())
} else if (waitingRemove > 0) {
if (dataArr.isNotEmpty()) {
waitingRemove--
removeFirstMessage()
return@AnimInfo
}
waitingRemove = 0
}
})
}
還有一些小問題沒有處理好,比如短時間內連續呼叫 addMessage 會導致等待洗掉的任務過多(雖然已經做了兜底處理),比如圖片加載沒有做中斷處理,
各位如果想要上到業務環境還需將這些問題完善,
原始碼地址:點擊這里

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/383018.html
標籤:其他
上一篇:Hive常用的UDF開發
