写点什么

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

作者:anyRTC开发者
  • 2022 年 3 月 24 日
  • 本文字数:4172 字

    阅读完需:约 14 分钟

预期效果


实现思路

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


  • 一个不停变速伸缩的扇形动画

  • 一个固定速度的旋转动画


扇形可以通过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:



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


  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



发布于: 刚刚阅读数: 2
用户头像

实时交互,万物互联! 2020.08.10 加入

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

评论

发布
暂无评论
Android技术分享|【自定义View】实现Material Design的Loading效果_android_anyRTC开发者_InfoQ写作平台