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

语言: CN / TW / HK

预期效果

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

实现思路

分析一下这个动画,效果应该是通过两个动画来实现的。 * 一个不停变速伸缩的扇形动画 * 一个固定速度的旋转动画

扇形可以通过canvas#drawArc来实现

旋转动画可以用setMatrix实现

圆角背景可以通过canvas#drawRoundRect实现

还需要一个计时器来实现动画效果

这个View最好能够更方便的修改样式,所以需要定义一个declare-styleable,方便通过布局来修改属性。 这些元素应该包括: * 最底层的卡片颜色 * 卡片内变局 * 内部长条的颜色 * 长条的粗细 * 长条的距离中心的半径 * 字体大小 * 字体颜色

因为用到动画,避免掉帧,最好离屏绘制到缓冲帧上,再通知view绘制缓冲帧。

代码实现

  1. 定义一下styleable xml <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 Kotlin 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定时器 ```Kotlin 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 } ```

    定义一个数据模型 Kotlin 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// 计算完当前帧数据后的回调 ) 动画插值器 Kotlin private fun interpolator(x: Float) = x * x * (3 - 2 * 2) 4. 定义初始化缓冲帧 此方法在外部调用显示loading时调用即可,调用前需判断是否已经初始化 Kotlin private fun initCanvas() { bufferBitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888) bufferCanvas = Canvas(bufferBitmap) } 5. 实现扇形的绘制 ```Kotlin 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. 实现扇形整体缓慢转圈Kotlin 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方法,方便外部使用

    展示 ```Kotlin 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 } ```

关闭 ```Kotlin fun dismissProgress() { if (!showing) return

purgeTimer() showing = false visibility = GONE } ```

最后看一下View#onDraw的实现: ```Kotlin 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

在这里插入图片描述