Android技術|「自定義View」實現Material Design的Loading效果

預期效果

Android技術|「自定義View」實現Material Design的Loading效果

Android技術|「自定義View」實現Material Design的Loading效果

實現思路

分析一下這個動畫,效果應該是透過兩個動畫來實現的。

一個不停變速伸縮的扇形動畫

一個固定速度的旋轉動畫

扇形可以透過

canvas#drawArc

來實現

旋轉動畫可以用

setMatrix

實現

圓角背景可以透過

canvas#drawRoundRect

實現

還需要一個計時器來實現動畫效果

這個View最好能夠更方便的修改樣式,所以需要定義一個declare-styleable,方便透過佈局來修改屬性。

這些元素應該包括:

最底層的卡片顏色

卡片內變局

內部長條的顏色

長條的粗細

長條的距離中心的半徑

字型大小

字型顏色

因為用到動畫,避免掉幀,最好離屏繪製到緩衝幀上,再通知view繪製緩衝幀。

程式碼實現

1。 定義一下styleable

2。 在程式碼中解析styleable

init {val defCircleRadius = context。resources。getDimension(R。dimen。dp24)val defCardColor = Color。WHITEval 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 = defCircleRadiuscardColor = defCardColorcardPadding = defCardPaddingstrokeWidth = defStrokeWidthstrokeColor = defStrokeColortextSize = defTextSizetextColor = defTextColor}paint。textSize = textSizeif (text。isNotBlank())textWidth = paint。measureText(text)}

3。 實現一個計時器,再定義一個數據型別來儲存動畫相關資料,還有一個動畫插值器

Timer定時器

private fun startTimerTask() {val t = Timer()t。schedule(object : TimerTask() {override fun run() {if (taskList。isEmpty())returnval taskIterator = taskList。iterator()while (taskIterator。hasNext()) {val task = taskIterator。next()task。progress += 17if (task。progress > task。duration) {task。progress = task。duration}if (task。progress == task。duration) {if (!task。convert) {task。startAngle -= 40if (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}

定義一個數據模型

private data class AnimTask(var startAngle: Int = 0,// 扇形繪製起點val duration: Int = 700,// 動畫時間var progress: Int = 0,// 動畫已執行時間var interpolatorProgress: Float = 0f,// 插值器計算後的值,取值0。0f ~ 1。0fvar progressFloat: Float = 0f,// 取值0。0f ~ 1。0fvar 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 = strokeWidthpaint。color = strokeColorpaint。strokeCap = Paint。Cap。ROUNDpaint。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:

Android技術|「自定義View」實現Material Design的Loading效果

到這裡,核心功能基本就完成了。

7。 定義一個

showProgress

方法以及

dismissProgress

方法,方便外部使用

展示

fun showProgress() {if (showing)returnif (!this::bufferBitmap。isInitialized) {initCanvas()}taskList。add(AnimTask {drawFrame(it)})taskList。add(AnimTask(duration = 5000) {drawRotation(it)})startTimerTask()showing = truevisibility = VISIBLE}

關閉

fun dismissProgress() {if (!showing)returnpurgeTimer()showing = falsevisibility = 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 + cardPaddingrectF。set(centerX - rectHalfDimension,centerY - rectHalfDimension,centerX + rectHalfDimension,if (text。isNotBlank()) centerY + paint。textSize + rectHalfDimension else centerY + rectHalfDimension)paint。color = cardColorpaint。style = Paint。Style。FILLcanvas。drawRoundRect(rectF, 12f, 12f, paint)if (text。isNotBlank()) {val dx = measuredWidth。shr(1) - textWidth / 2paint。color = textColorcanvas。drawText(text, dx, rectF。bottom - paint。textSize, paint)}if (this::bufferBitmap。isInitialized)canvas。drawBitmap(bufferBitmap, bufferMatrix, paint)}

原始碼請移步:[ARCallPlus](https://github。com/anyRTC-UseCase/ARCallPlus)。

Android技術|「自定義View」實現Material Design的Loading效果