在之前的文章中我们实现了自定义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() private val dataArr = linkedList()
使用一个 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 会导致等待删除的任务过多(虽然已经做了兜底处理),比如图片加载没有做中断处理。
各位如果想要上到业务环境还需将这些问题完善。
源码地址:点击这里
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)