预期作用

Android技术分享|【自定义View】实现Material Design的Loading效果
Android技术分享|【自定义View】实现Material Design的Loading效果

完成思路

分析一下这个动画,作用应该是经过两个动画来完成的。

  • 一个不停变速伸缩的扇形动画
  • 一个固定速度的旋转动画

扇形可以经过canvas#drawArc来完成

旋转动画可以用setMatrix完成

圆角背景可以经过canvas#drawRoundRect完成

还需求一个计时器来完成动画作用

这个View最好可以更便利的修正样式,所以需求界说一个declare-styleable,便利经过布局来修正特点。 这些元素应该包含:

  • 最底层的卡片色彩
  • 卡片内变局
  • 内部长条的色彩
  • 长条的粗细
  • 长条的距离中心的半径
  • 字体大小
  • 字体色彩

因为用到动画,避免掉帧,最好离屏制作到缓冲帧上,再告诉view制作缓冲帧。

代码完成

  1. 界说一下styleable
<declare-styleable name="MaterialLoadingProgress">
    <attr name="loadingProgress_circleRadius" format="dimension" />
    <attr name="loadingProgress_cardColor" format="color" />
    <attr name="loadingProgress_cardPadding" format="dimension" />
    <attr name="loadingProgress_strokeWidth" format="dimension" />
    <attr name="loadingProgress_strokeColor" format="color" />
    <attr name="loadingProgress_text" format="string" />
    <attr name="loadingProgress_textSize" format="dimension" />
    <attr name="loadingProgress_textColor" format="color" />
</declare-styleable>
  1. 在代码中解析styleable
init {
  val defCircleRadius = context.resources.getDimension(R.dimen.dp24)
  val defCardColor = Color.WHITE
  val defCardPadding = context.resources.getDimension(R.dimen.dp12)
  val defStrokeWidth = context.resources.getDimension(R.dimen.dp5)
  val defStrokeColor = ContextCompat.getColor(context, R.color.teal_200)
  val defTextSize = context.resources.getDimension(R.dimen.sp14)
  val defTextColor = Color.parseColor("#333333")
  if (attrs != null) {
    val attrSet = context.resources.obtainAttributes(attrs, R.styleable.MaterialLoadingProgress)
    circleRadius = attrSet.getDimension(R.styleable.MaterialLoadingProgress_loadingProgress_circleRadius, defCircleRadius)
    cardColor = attrSet.getColor(R.styleable.MaterialLoadingProgress_loadingProgress_cardColor, defCardColor)
    cardPadding = attrSet.getDimension(R.styleable.MaterialLoadingProgress_loadingProgress_cardPadding, defCardPadding)
    strokeWidth = attrSet.getDimension(R.styleable.MaterialLoadingProgress_loadingProgress_strokeWidth, defStrokeWidth)
    strokeColor = attrSet.getColor(R.styleable.MaterialLoadingProgress_loadingProgress_strokeColor, defStrokeColor)
    text = attrSet.getString(R.styleable.MaterialLoadingProgress_loadingProgress_text) ?: ""
    textSize = attrSet.getDimension(R.styleable.MaterialLoadingProgress_loadingProgress_textSize, defTextSize)
    textColor = attrSet.getColor(R.styleable.MaterialLoadingProgress_loadingProgress_textColor, defTextColor)
    attrSet.recycle()
  } else {
    circleRadius = defCircleRadius
    cardColor = defCardColor
    cardPadding = defCardPadding
    strokeWidth = defStrokeWidth
    strokeColor = defStrokeColor
    textSize = defTextSize
    textColor = defTextColor
  }
  paint.textSize = textSize
  if (text.isNotBlank())
    textWidth = paint.measureText(text)
}
  1. 完成一个计时器,再界说一个数据类型来存储动画相关数据,还有一个动画插值器

Timer定时器

private fun startTimerTask() {
  val t = Timer()
  t.schedule(object : TimerTask() {
    override fun run() {
      if (taskList.isEmpty())
        return
      val taskIterator = taskList.iterator()
      while (taskIterator.hasNext()) {
        val task = taskIterator.next()
        task.progress += 17
        if (task.progress > task.duration) {
          task.progress = task.duration
        }
        if (task.progress == task.duration) {
          if (!task.convert) {
            task.startAngle -= 40
            if (task.startAngle < 0)
              task.startAngle += 360
          }
          task.progress = 0
          task.convert = !task.convert
        }
        task.progressFloat = task.progress / task.duration.toFloat()
        task.interpolatorProgress = interpolator(task.progress / task.duration.toFloat())
        task.currentAngle = (320 * task.interpolatorProgress).toInt()
        post { task.onProgress(task)  }
      }
    }
  }, 0, 16)
  timer = t
}

界说一个数据模型

private data class AnimTask(
  var startAngle: Int = 0,// 扇形制作起点
  val duration: Int = 700,// 动画时间
  var progress: Int = 0,// 动画已执行时间
  var interpolatorProgress: Float = 0f,// 插值器核算后的值,取值0.0f ~ 1.0f
  var progressFloat: Float = 0f,// 取值0.0f ~ 1.0f
  var convert: Boolean = false,// 判别扇形的制作进程,为true时反向制作
  var currentAngle: Int = 0,// 制作扇形运用
  val onProgress: (AnimTask) -> Unit// 核算完当时帧数据后的回调
)

动画插值器

private fun interpolator(x: Float) = x * x * (3 - 2 * 2)
  1. 界说初始化缓冲帧

此办法在外部调用显现loading时调用即可,调用前需判别是否已经初始化

private fun initCanvas() {
  bufferBitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888)
  bufferCanvas = Canvas(bufferBitmap)
}
  1. 完成扇形的制作
private fun drawFrame(task: AnimTask) {
  bufferBitmap.eraseColor(Color.TRANSPARENT)
  val centerX = measuredWidth.shr(1)
  val centerY = measuredHeight.shr(1)
  rectF.set(
    centerX - circleRadius, centerY - circleRadius,
    centerX + circleRadius, centerY + circleRadius
  )
  paint.strokeWidth = strokeWidth
  paint.color = strokeColor
  paint.strokeCap = Paint.Cap.ROUND
  paint.style = Paint.Style.STROKE
  // 这里的判别,对应扇形逐渐延长、及逐渐缩短
  if (task.convert) {
    bufferCanvas.drawArc(
      rectF, task.startAngle.toFloat(), -(320.0f - task.currentAngle.toFloat()), false, paint
    )
  } else {
    bufferCanvas.drawArc(
      rectF, task.startAngle.toFloat(), task.currentAngle.toFloat(), false, paint
    )
  }
  invalidate()
}
  1. 完成扇形整体缓慢转圈
private fun drawRotation(task: AnimTask) {
  val centerX = measuredWidth.shr(1)
  val centerY = measuredHeight.shr(1)
  bufferMatrix.reset()
  bufferMatrix.postRotate(task.progressFloat * 360f, centerX.toFloat(), centerY.toFloat())
  bufferCanvas.setMatrix(bufferMatrix)
}

一定要记住调用matrix#reset

否则作用就会像这样 XD:

Android技术分享|【自定义View】实现Material Design的Loading效果

到这里,中心功用基本就完成了。

  1. 界说一个showProgress办法以及dismissProgress办法,便利外部运用

展现

fun showProgress() {
  if (showing)
    return
  if (!this::bufferBitmap.isInitialized) {
    initCanvas()
  }
  taskList.add(AnimTask {
    drawFrame(it)
  })
  taskList.add(AnimTask(duration = 5000) {
    drawRotation(it)
  })
  startTimerTask()
  showing = true
  visibility = VISIBLE
}

关闭

fun dismissProgress() {
  if (!showing)
    return
  purgeTimer()
  showing = false
  visibility = GONE
}

最终看一下View#onDraw的完成:

override fun onDraw(canvas: Canvas) {
  val centerX = measuredWidth.shr(1)
  val centerY = measuredHeight.shr(1)
  val rectHalfDimension = if (circleRadius > textWidth / 2f) circleRadius + cardPadding else textWidth / 2f + cardPadding
  rectF.set(
    centerX - rectHalfDimension,
    centerY - rectHalfDimension,
    centerX + rectHalfDimension,
    if (text.isNotBlank()) centerY + paint.textSize + rectHalfDimension else centerY + rectHalfDimension
  )
  paint.color = cardColor
  paint.style = Paint.Style.FILL
  canvas.drawRoundRect(rectF, 12f, 12f, paint)
  if (text.isNotBlank()) {
    val dx = measuredWidth.shr(1) - textWidth / 2
    paint.color = textColor
    canvas.drawText(text, dx, rectF.bottom - paint.textSize, paint)
  }
  if (this::bufferBitmap.isInitialized)
    canvas.drawBitmap(bufferBitmap, bufferMatrix, paint)
}

源代码请移步:ARCallPlus。

Android技术分享|【自定义View】实现Material Design的Loading效果