Android Technology Sharing | [Custom View] Realize the Loading effect of Material Design

expected result

insert image description here insert image description here

Realize ideas

Analyze this animation, the effect should be achieved through two animations.

  • A fan-shaped animation that stretches and shrinks continuously
  • A fixed speed rotation animation

Sector can be canvas#drawArcachieved by

Rotation animation can be setMatriximplemented with

A rounded background can be canvas#drawRoundRectachieved by

Also need a timer to animate the effect

This View is better to be able to modify the style more conveniently, so you need to define a declare-styleable, which is convenient to modify the properties through the layout. These elements should include:

  • bottom card color
  • Card changes
  • The color of the inner bar
  • the thickness of the strip
  • The radius of the bar from the center
  • font size
  • font color

Because animation is used to avoid dropped frames, it is best to draw off-screen to the buffer frame, and then notify the view to draw the buffer frame.

Code

  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>
复制代码
  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) {
    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. Implement a timer, define a data type to store animation-related data, and an animation interpolator

Timer 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
}
复制代码

define a data model

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// 计算完当前帧数据后的回调
)
复制代码

Animation Interpolator

private fun interpolator(x: Float) = x * x * (3 - 2 * 2)
复制代码
  1. Define the initial buffer frame

This method can be called when the external call shows loading. Before calling, it is necessary to judge whether it has been initialized

private fun initCanvas() {
  bufferBitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888)
  bufferCanvas = Canvas(bufferBitmap)
}
复制代码
  1. Realize the drawing of a fan
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. Realize the overall slow circle of the sector
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)
}
复制代码

Remember to callmatrix#reset

Otherwise the effect will look like this XD:

insert image description here

At this point, the core functions are basically completed.

  1. Define a showProgressmethod and dismissProgressmethod for external use

exhibit

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
}
复制代码

closure

fun dismissProgress() {
  if (!showing)
    return
                       
  purgeTimer()
  showing = false
  visibility = GONE
}
复制代码

Finally look at View#onDrawthe implementation:

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)
}
复制代码

Please move to the source code: ARCallPlus .

insert image description here

Guess you like

Origin juejin.im/post/7078483878259720199