概述
今天利用之前的知识手写一个支付宝支付成功的简单动画。
效果图
path_anim.gif知识点
- Canvas使用
- Paint使用
- Path
- PathMeasure
- ValueAnimator
PathMeasure
其他知识点前面都提过了,这里主要讲一下PathMeasure的简单使用,更详细的内容后续会专门出一篇文章。
PathMeasure主要是结合Path使用,主要的用处是用来截取Path的一部分,用于绘制路径或者其他用途,这里我们就结合这个支付成功的动画来看PathMeasure的具体使用,这里主要使用到的相关函数是
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
具体使用效果我们接着往下
设计思路
绘制这个动画,我们首先将路径分为2条路径,第一条路径就是外部的圆圈,第二条路径就是内部的勾形线段,我们只需要在动画执行的过程中,截取相对应的路径,并根据动画执行的时间来绘制出截取后的路径,就可以完成这个动画操作。所以简单梳理一下实现该效果的步骤
- 1.创建外部的圆形路径对象,并获取到对应的PathMeasure
- 2.创建内部的勾形路径对象,并获取到对应的PathMeasure
- 3.执行动画时,利用ValueAnimator来监听获取当前的动画值,进而开始计算两条路径各自应该截取多少长度并进行绘制
- 4.开始执行动画
Coding(今天心情好用kotlin写吧)
1.初始化画笔
// 创建画笔
var paint: Paint = Paint()
// 构造方法就初始化画笔
init {
// 不填充
paint.style = Paint.Style.STROKE
// 画笔宽度
paint.strokeWidth = 5f
// 画笔颜色
paint.color = Color.WHITE
// 画笔笔头的样式,这里是圆形
paint.strokeJoin = Paint.Join.ROUND
// 开启抗锯齿
paint.isAntiAlias = true
// 关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
2.开始定义需要的一些路径变量和动画变量
// 绘制圆形路径
lateinit var circlePath: Path
// 获取圆形路径的PathMeasure
lateinit var circlePathMeasure: PathMeasure
// 绘制圆形路径时的起始点离开始点的路径长度
var circleStart = 0.0f
// 绘制圆形路径时需要绘制的长度
var circleOffset = 0.0f
// 绘制勾形路径
lateinit var path: Path
// 获取勾形路径的PathMeasure
lateinit var pathMeasure: PathMeasure
// 绘制勾形路径时的起始点离开始点的路径长度
var pathStart = 0.0f
// 绘制勾形路径时需要绘制的长度
var pathOffset = 0.0f
// 当前动画的value值
var currentValue = 0.0f
// 动画执行时长
private var duration = 1000L
3.初始化两条路径
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
initPath(w.toFloat(), h.toFloat())
}
private fun initPath(w: Float, h: Float) {
var radius = w / 2 - 5
// 构造圆形路径
circlePath = Path()
circlePath.addCircle(w / 2, h / 2, radius, Path.Direction.CW)
circlePathMeasure = PathMeasure(circlePath, true)
// 构造勾形路径
path = Path();
path.moveTo(w / 2 - radius / 2, h / 2)
path.lineTo(w / 2, height / 2 + radius / 2)
path.lineTo(w / 2 + radius / 2, h / 2 - radius / 3)
pathMeasure = PathMeasure(path, false)
}
我们在onSizeChanged()函数回调时来确认具体的路径的长度,不了解onSizeChanged()函数执行时机的可以看Android自定义View(1) 《自定义View重要方法及参数说明》,这里我们使用有点不合理的是我把半径默认设置为宽度一半减去5px,但是其实在未知控件宽高的情况下这样设置是不合理的,我这里只是为了方便大家查看,所以自己使用的时候要小心。
4.绘制函数
// 绘制路径
private fun drawPath(canvas: Canvas?) {
var path1 = Path()
circlePathMeasure.getSegment(circleStart, circleOffset, path1, true)
var path2 = Path()
pathMeasure.getSegment(pathStart, pathOffset, path2, true)
canvas?.drawPath(path1, paint)
canvas?.drawPath(path2, paint)
}
其实PathMeasure主要用于这里,我们创建了一个path1和path2的Path对象,这个对象用来保存截取后的路径信息,所以从这里也可以看出来,这里是和C++的函数中使用指针的值传递的效果是一致的,我们获取到截取后的path对象后就进行绘制。
5.开始编写动画
// 开始执行动画
fun startAnim() {
var valueAnimator = ValueAnimator.ofFloat(0f, 500f)
valueAnimator.duration = duration
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.addUpdateListener { animation ->
currentValue = animation?.animatedValue as Float
if (currentValue <= 300) {
circleOffset = circlePathMeasure.length*(currentValue/300)
pathOffset = 0f
}else {
circleOffset = circlePathMeasure.length
pathOffset = pathMeasure.length*((currentValue-300)/200)
}
invalidate()
}
valueAnimator.start()
}
计算方式很简单,我们创建了一个0-500的值动画,当在0-300时我们会截取第一条路径,第二条路径截取长度始终为0,然后在300~500的范围内我们会把第一条路径完整绘制出来,第二条路径开始进行截取,当动画结束时第二条路径就截取到了完整路径。
完整代码
package com.tx.camera.view
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator
/**
* create by xu.tian
* @date 2021/9/5
*/
class MyLoadingView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
// 创建画笔
private var paint: Paint = Paint()
init {
// 不填充
paint.style = Paint.Style.STROKE
// 画笔宽度
paint.strokeWidth = 5f
// 画笔颜色
paint.color = Color.WHITE
// 画笔笔头的样式,这里是圆形
paint.strokeJoin = Paint.Join.ROUND
// 开启抗锯齿
paint.isAntiAlias = true
// 关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
// 绘制圆形路径
lateinit var circlePath: Path
// 获取圆形路径的PathMeasure
lateinit var circlePathMeasure: PathMeasure
// 绘制圆形路径时的起始点离开始点的路径长度
var circleStart = 0.0f
// 绘制圆形路径时需要绘制的长度
var circleOffset = 0.0f
// 绘制勾形路径
lateinit var path: Path
// 获取勾形路径的PathMeasure
lateinit var pathMeasure: PathMeasure
// 绘制勾形路径时的起始点离开始点的路径长度
var pathStart = 0.0f
// 绘制勾形路径时需要绘制的长度
var pathOffset = 0.0f
// 当前动画的value值
var currentValue = 0.0f
// 动画执行时长
private var duration = 1000L
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
drawPath(canvas)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
initPath(w.toFloat(), h.toFloat())
}
private fun initPath(w: Float, h: Float) {
var radius = w / 2 - 5
// 构造圆形路径
circlePath = Path()
circlePath.addCircle(w / 2, h / 2, radius, Path.Direction.CW)
circlePathMeasure = PathMeasure(circlePath, true)
// 构造勾形路径
path = Path();
path.moveTo(w / 2 - radius / 2, h / 2)
path.lineTo(w / 2, height / 2 + radius / 2)
path.lineTo(w / 2 + radius / 2, h / 2 - radius / 3)
pathMeasure = PathMeasure(path, false)
}
// 绘制路径
private fun drawPath(canvas: Canvas?) {
var path1 = Path()
circlePathMeasure.getSegment(circleStart, circleOffset, path1, true)
var path2 = Path()
pathMeasure.getSegment(pathStart, pathOffset, path2, true)
canvas?.drawPath(path1, paint)
canvas?.drawPath(path2, paint)
}
// 开始执行动画
fun startAnim() {
var valueAnimator = ValueAnimator.ofFloat(0f, 500f)
valueAnimator.duration = duration
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.addUpdateListener { animation ->
currentValue = animation?.animatedValue as Float
if (currentValue <= 300) {
circleOffset = circlePathMeasure.length*(currentValue/300)
pathOffset = 0f
}else {
circleOffset = circlePathMeasure.length
pathOffset = pathMeasure.length*((currentValue-300)/200)
}
invalidate()
}
valueAnimator.start()
}
}
思考
- Q:我们在这里将圆形路径和勾形路径分为了两个路径,不能用一个路径实现吗?
A:当然是可以的,我们可以使用nextContour()来进行路径的遍历,但是这里使用两个是为了扩展性更强,比如当你在加载的时候不知道加载的结果你可能只需要进行外部圆形路径的绘制,那么你就可以灵活的自由选择各个路径的组合,比如你想绘制一个加载失败的动画,那么你可以再创建一个path在圆的中心进行绘制一个加载失败的图形,这样的组合使用更加灵活。
总结
今天的知识回顾就写到这里了,下篇再写一下怎么绘制文字吧。
网友评论