Android技术分享| 自习室自定义View代替通知动画(完)

Posted anyRTC

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android技术分享| 自习室自定义View代替通知动画(完)相关的知识,希望对你有一定的参考价值。

在之前的文章中我们实现了自定义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 会导致等待删除的任务过多(虽然已经做了兜底处理),比如图片加载没有做中断处理。

各位如果想要上到业务环境还需将这些问题完善。

源码地址:点击这里

以上是关于Android技术分享| 自习室自定义View代替通知动画(完)的主要内容,如果未能解决你的问题,请参考以下文章

Android技术分享| 自习室自定义View代替通知动画

Android技术分享| 自习室自定义View代替通知动画(完)

Android技术分享| 自习室自定义View代替通知动画

Android技术分享| Android 自定义View多人视频通话控件

Android技术分享| Android 自定义View多人视频通话控件

Android进阶技术分享之——自定义 View 系列实战篇-ViewGroup(内含自定义View 宝藏图)