expected result
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#drawArc
achieved by
Rotation animation can be setMatrix
implemented with
A rounded background can be canvas#drawRoundRect
achieved 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
- 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>
复制代码
- 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)
}
复制代码
- 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)
复制代码
- 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)
}
复制代码
- 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()
}
复制代码
- 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 call
matrix#reset
Otherwise the effect will look like this XD:
At this point, the core functions are basically completed.
- Define a
showProgress
method anddismissProgress
method 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#onDraw
the 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 .