1
Android 技术分享| 【自习室】自定义 View 代替通知动画(完)
 作者:anyRTC开发者
- 2021 年 12 月 16 日
 本文字数:2997 字
阅读完需:约 10 分钟
在之前的文章中我们实现了自定义 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 会导致等待删除的任务过多(虽然已经做了兜底处理),比如图片加载没有做中断处理。
各位如果想要上到业务环境还需将这些问题完善。
源码地址:点击这里
 划线
评论
复制
发布于: 16 小时前阅读数: 8
版权声明: 本文为 InfoQ 作者【anyRTC开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/7c82331b46ca08301fa7b608b】。文章转载请联系作者。
anyRTC开发者
关注
实时交互,万物互联! 2020.08.10 加入
实时交互,万物互联,全球实时互动云服务商领跑者!











    
评论