预期效果

在这里插入图片描述
在这里插入图片描述

实现思路

分析一下这个动画,效果应该是通过两个动画来实现的。

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

扇形可以通过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>
  2. 在代码中解析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)
    }
  3. 实现一个计时器,再定义一个数据类型来存储动画相关数据,还有一个动画插值器

    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)
  4. 定义初始化缓冲帧

    此方法在外部调用显示loading时调用即可,调用前需判断是否已经初始化
    private fun initCanvas() {
      bufferBitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888)
      bufferCanvas = Canvas(bufferBitmap)
    }
  5. 实现扇形的绘制

    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()
    }
  6. 实现扇形整体缓慢转圈

    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:

在这里插入图片描述

到这里,核心功能基本就完成了。

  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

在这里插入图片描述


anyRTC
343 声望5 粉丝

实时交互,万物互联,全球实时互动云服务商领跑者!