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

上篇文章我们完成了一条信息的测量和绘制,本篇我们来实现消息的平移动画

效果图如下:
Android技术分享| 【自习室】自定义View代替通知动画(2)

在自定义View中,通常我比较喜欢额外创建一个Bitmap和一个Canvas来绘制动画效果。大家可以根据自己喜好修改,实现的方式有很多。

首先在首次测量的时候我们创建Canvas、Matrix、Bitmap,如果你的实际使用场景中,View的大小可能会更改,这里也可以每次测量都重新创建。

首先声明3个变量:

private lateinit var mBufferBitmap: Bitmap
private lateinit var mBufferCanvas: Canvas
private lateinit var mBufferMatrix: Matrix

onMeasure中创建:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
  // 测量代码... 
                                                                                            
  if (!this::mBufferBitmap.isInitialized) {
    mBufferBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    mBufferCanvas = Canvas(mBufferBitmap)
    mBufferMatrix = Matrix()
  }
}

接下来我们修改一下消息数据模型,将每条消息的动画进度以及其他一些属性存储起来,以便后续拓展和修改。

data class Message(
  val avatar: String,// 头像地址
  val nickname: String,// 昵称
  val joinRoom: Int,// 1=加入,否则均为退出,改为Boolean也可
  var info: NicknameInfo,// 存储本条消息的宽高等信息
  var shader: BitmapShader? = null,// 图片加载相关
  var bitmap: Bitmap? = null,
  var life: Int = 5,// 消息存活时间
  val timing: Long = System.currentTimeMillis(),// 已消耗时间
  var xProgress: Float = 1f,// x轴平移比例,取值1.0f~0.0f
  var yProgress: Float = 0f,// y轴平移比例,同上
)
data class NicknameInfo(
  val nickname: String,// 修改后的nickname(超过5个字符,后面变为省略号
  val nicknameWidth: Float,// 昵称宽度
  val messageWidth: Float,// 消息总宽
  val statusTextWidth: Float// "加入房间"、"退出房间",这几个字的宽度。根据实际需求这里也可以改为全局变量,因为字宽基本上可以说是固定的
)

数据的命名和定义可以完全按照自己的喜好来,存储需要存储的数据即可。无论是自定义View还是其他,最终总是要落实到数据上的,定义好存储数据的结构和算法即可。

根据我们上面定义的数据,我们用mBufferCanvas和mBufferMatrix进行一个渲的染:p

private fun drawMessage() {
  mBufferMatrix.reset()// 绘制前reset一下matrix,清空一下buffer bitmap。
  // 这里使用 mBufferBitmap.eraseColor(Color.TRANSPARENT)也是可以的,至于两者有什么不同,我也不清楚(; ̄ェ ̄),欢迎留言补充
  mBufferCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
                                                                                        
  val msg = messageList[0]// 上一篇文章中,我们使用的是一个全局变量,这里我偷偷换成了List,用其他形式也可以,因为需求中是显示两条消息,用数组也行,两个全局变量也可以,根据自己喜好即可
  val info = msg.info
  val xOffset = msg.xProgress * info.messageWidth
  val yOffset = msg.yProgress * messageHeight + msg.yProgress * messagePadding
  mBufferMatrix.setTranslate(xOffset, yOffset)// 平移效果通过matrix来实现,也可以直接在绘制的时候加上偏移量,我本来是这么做的,但创建的mBufferMatrix就显得很多余,我就改成用matrix了(´・ω・`)
  mBufferCanvas.setMatrix(mBufferMatrix)// 记得设置上
  drawMsg(// 绘制消息的代码我丢在一个方法里了,这样我绘制两条再调用一次drawMsg就行了,我可不想复制两遍代码(´・_・`)
    msg,
    info.messageWidth,
    info.nicknameWidth,
    info.nickname
  )

  post { invalidate() }// 防止子线程调用,进行一个post
}

/**
 * DO NOT CALL THIS FUNCTION
 */
private fun drawMsg(
  msg: Message,
  messageWidth: Float,
  nicknameWidth: Float,
  nickname: String
) {
  path.reset()// 用前先reset,好习惯
  paint.color = Color.parseColor("#F3F3F3")

  val statusText = if (msg.joinRoom == 1) "进入直播间" else "退出直播间"//状态直接传进来也可,个人喜好
  // 接下来你看到的所有xOffset、yOffset,都是我改用matrix之前的逻辑,把0f删掉、把x/yOffset取消注释,就变成不用matrix的版本了(´・ω・`)
  val messageLeft = measuredWidth - messageWidth// + xOffset
  path.addArc(// 给path添加一个半圆
    messageLeft,
    //yOffset,
    0f,
    messageLeft + avatarPadding + avatarHeight.toFloat(),
    /*yOffset*/0f + messageHeight.toFloat(),
    90f,
    180f
  )
  // 给path添加矩形
  path.moveTo(messageLeft + avatarHeight.shr(1).toFloat(), /*yOffset*/0f)
  path.lineTo(measuredWidth.toFloat(), /*yOffset*/0f)
  path.lineTo(measuredWidth.toFloat(), /*yOffset*/0f + messageHeight.toFloat())
  path.lineTo(
    messageLeft + avatarHeight.shr(1).toFloat(), /*yOffset*/
    0f + messageHeight.toFloat()
  )

  paint.color = Color.parseColor("#434343")// 背景色
  mBufferCanvas.drawPath(path, paint)// 填充

  // 绘制文本
  paint.color = Color.WHITE
  mBufferCanvas.drawText(
    statusText,
    messageLeft + avatarHeight + avatarPadding.shl(1) + nicknameWidth + messagePadding,
    //(measuredWidth - statusTextWidth - statusTextPadding) + /*xOffset*/上面提到放开offset的算法,这里的注视就没删除,留下做参考0f,
    messageHeight.shr(1) + fontCenterOffset + /*yOffset*/0f,
    paint
  )
                                                                                                  
  paint.color = Color.parseColor("#BCBCBC")// 绘制昵称
  mBufferCanvas.drawText(
    nickname,
    messageLeft + avatarPadding.shl(1) + avatarHeight,
    //(messageWidth - statusTextWidth - statusTextPadding.shl(1) - nicknameWidth) + /*xOffset*/0f,
    messageHeight.shr(1) + fontCenterOffset + /*yOffset*/0f,
    paint
  )
                                                                                                  
  msg.bitmap?.let {// 图片加载完成的话,绘制头像
    mBufferCanvas.save()
    paint.shader = msg.shader
    val translateOffset = (messageHeight - it.width).shr(1)
    mBufferCanvas.translate(
      messageLeft + translateOffset,
      /*yOffset*/0f + translateOffset.toFloat()
    )
    mBufferCanvas.drawCircle(
      it.width.shr(1).toFloat(),
      it.width.shr(1).toFloat()/*messageHeight.shr(1).toFloat()*/,
      avatarHeight.shr(1).toFloat(),
      paint
    )
    paint.shader = null
    mBufferCanvas.restore()
  }
}

完成上面的代码后只需要修改x轴和y轴的变量,即可实现"动画"了。动手试试吧(´・ω・`)

下篇文章我们来实现添加消息、计时、生命结束后删除消息等功能,还有真正的动画效。

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

上一篇:Android绘图(三)双缓存技术


下一篇:2021-10-28