The desired effect

Implementation approach

Analyzing the animation, the effect should be achieved through two animations.

  • A fan-shaped animation that keeps shifting
  • A fixed speed rotation animation

The sector can be implemented by canvas#drawArc

Rotation animation can be implemented using setMatrix

Rounded backgrounds can be achieved with canvas#drawRoundRect

You also need a timer to animate

The View should be easier to style, so you need to define a declare-styleable that allows you to modify properties through layout. These elements should include:

  • The bottom card color
  • Changes in the card
  • Color of internal strips
  • The thickness of a strip
  • The radius of the strip from the center
  • The font size
  • The font color

To avoid dropping frames, it is best to draw the buffer frame off-screen and then tell the view to draw the buffer frame.

Code implementation

  1. Define 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>
Copy the code
  1. Parse styleable in code
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) {
    valattrSet = 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)
}
Copy the code
  1. Implement a timer, define a data type to store animation-related data, and an animation interpolator

The Timer Timer

private fun startTimerTask(a) {
  val t = Timer()
  t.schedule(object : TimerTask() {
    override fun run(a) {
      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 = 0task.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
}
Copy the code

Define a data model

private data class AnimTask(
  var startAngle: Int = 0.// Fan the starting point
  val duration: Int = 700.// Animation time
  var progress: Int = 0.// The animation has been executed
  var interpolatorProgress: Float = 0f.// The calculated value of the interpolator, ranging from 0.0f to 1.0f
  var progressFloat: Float = 0f.// The value ranges from 0.0f to 1.0f
  var convert: Boolean = false.// Determine the fan drawing process, if true, reverse drawing
  var currentAngle: Int = 0.// Draw the fan to use
  val onProgress: (AnimTask) -> Unit// The callback after the current frame is calculated
)
Copy the code

Animation interpolator

private fun interpolator(x: Float) = x * x * (3 - 2 * 2)
Copy the code
  1. Define the initialization buffer frame

This method can be called when loading is displayed in the external call. Before calling this method, check whether it is initialized

private fun initCanvas(a) {
  bufferBitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888)
  bufferCanvas = Canvas(bufferBitmap)
}
Copy the code
  1. To achieve the sector of the drawing
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

  // The judgment here corresponds to the sector gradually lengthening and shortening
  if (task.convert) {
    bufferCanvas.drawArc(
      rectF, task.startAngle.toFloat(), -(320.0 f - task.currentAngle.toFloat()), false, paint
    )
  } else {
    bufferCanvas.drawArc(
      rectF, task.startAngle.toFloat(), task.currentAngle.toFloat(), false, paint
    )
  }
  invalidate()
}
Copy the code
  1. Slow rotation of the fan-shaped whole
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)
}
Copy the code

Be sure to call matrix#reset

Otherwise it would look something like this: XD:

At this point, the core functionality is almost complete.

  1. To define ashowProgressMethods anddismissProgressMethod, convenient external use

show

fun showProgress(a) {
  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
}
Copy the code

Shut down

fun dismissProgress(a) {
  if(! showing)return
                       
  purgeTimer()
  showing = false
  visibility = GONE
}
Copy the code

Finally, take a look at the implementation of 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)
}
Copy the code

Source code: ARCallPlus.