Android技術分享|【自定義View】實現Material Design的Loading效果
預期效果
實現思路
分析一下這個動畫,效果應該是通過兩個動畫來實現的。 * 一個不停變速伸縮的扇形動畫 * 一個固定速度的旋轉動畫
扇形可以通過canvas#drawArc
來實現
旋轉動畫可以用setMatrix
實現
圓角背景可以通過canvas#drawRoundRect
實現
還需要一個計時器來實現動畫效果
這個View最好能夠更方便的修改樣式,所以需要定義一個declare-styleable,方便通過佈局來修改屬性。 這些元素應該包括: * 最底層的卡片顏色 * 卡片內變局 * 內部長條的顏色 * 長條的粗細 * 長條的距離中心的半徑 * 字型大小 * 字型顏色
因為用到動畫,避免掉幀,最好離屏繪製到緩衝幀上,再通知view繪製緩衝幀。
程式碼實現
-
定義一下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>
-
在程式碼中解析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) }
-
實現一個計時器,再定義一個數據型別來儲存動畫相關資料,還有一個動畫插值器
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:
到這裡,核心功能基本就完成了。
- 定義一個
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。
- Android技術分享| ViewPager2離屏載入,實現抖音上下視訊滑動
- Android技術分享| Activity 過渡動畫 — 讓切換更加炫酷
- Linux下玩轉nginx系列(七)---nginx如何實現限流功能
- 技術分享| 如何部署安裝分散式序列號生成器系統
- web技術分享| 【地圖】實現自定義的軌跡回放
- 解決方案| 快對講綜合排程系統
- 實時訊息RTM| 多活架構中的資料一致性問題
- Android技術分享| Context淺析
- Android技術分享| Context淺析
- 螢幕共享的實現與應用
- 技術分析| 即時通訊和實時通訊的區別
- IOS技術分享| ARCallPlus 開源專案(二)
- Android技術分享| Android 中部分記憶體洩漏示例及解決方案
- Android技術分享| 安卓3行程式碼,實現整套音視訊通話功能
- 行業分析| 快對講Poc方案的優勢
- Android技術分享|【自定義View】實現Material Design的Loading效果
- IOS技術分享| ARCallPlus 開源專案(一)
- web技術分享| WebRTC控制攝像機平移、傾斜和縮放
- Android技術分享| anyLive 開源專案
- Android技術分享| 【Android 自定義View】多人視訊通話控制元件