本篇文章主要记录最近实现的下载动画所学的知识
效果
成功效果
失败效果
原理
我们想要的下载动画,大概分为两个部分,第一个部分是下载中的动画,也就是滑动条滑动的动画,另一部分是下载结束的动画,也就是滑动条缩放和显示小勾的动画,那么具体如何实现呢,首先我们要根据外部下载速度来改变滑动条进度,执行滑动条滑动的动画,然后等待下载完成以后,执行下载完成动画
主要知识点
- 属性动画的相关知识点
- 如何实现点击事件的同时开启下载动画
- 如何控制动画顺序播放?
- 动画的具体实现
属性动画的相关知识点
属性动画,即ObjectAnimator
动画,这种动画不仅包含补间动画的全部功能,而且可用支持自定义View
的扩展,说到这里就必须数值发生器ValueAnimator
,正是由于这个数值发生器,不断生成变化的数值,当然这个数值可以是Float
类型,也可以是Int
类型,才实现控件属性的变化,这也是接下来实战时主要用到的知识点。
如何实现点击事件的同时开启下载动画
这里通过一个回调函数,声明在控件内部,然后在外部具体实现,并调用控件的下载动画实现点击事件同时开启下载动画
回调函数的声明,这里通过高阶函数实现,括号里的内容表示参数为空,返回值也为空
/**
* 回调函数
* */
var callBack:(() -> Unit)? = null
声明了回调函数,然后在点击事件中调用
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN){
//点击事件回调
callBack?.let{back ->
back()
}
}
return true
}
最后外部具体实现,也就是下载动画的开启
//回调函数的具体实现
mProgressView.callBack = {
downLoadData()
}
/**
* 下载动画
* */
private fun downLoadData(){
ValueAnimator.ofFloat(0f,1.0f).apply {
duration = 2000
addUpdateListener {
mProgressView.progress = it.animatedValue as Float
}
start()
}
}
这里下载动画通过点击事件开启以后,会不断获取下载的进度值并赋给控件内部的progress
变量,下载动画的进度值progress
声明如下,实现了set方法,里面判断下载是否结束,如果结束执行下载完成动画
/**
* 进度的变化因子
* */
var progress = 0f
set(value) {
field = value
if (progress < 1.0f) {
//正在下载
invalidate()
} else {
//开启完成动画
startFinishAnimator()
}
}
下载完成动画
/**
* 下载完成动画
* */
private fun startFinishAnimator(){
val toCircleAnim = ValueAnimator.ofFloat(0f,height / 2f).apply {
duration = 1000
addUpdateListener {
mCornerRadius = it.animatedValue as Float
invalidate()
}
start()
}
val translateAnim = ValueAnimator.ofFloat(0f,(width - height) / 2f).apply {
duration = 1000
addUpdateListener {
mTranValue = it.animatedValue as Float
invalidate()
}
start()
}
//裁剪动画
val clipAnim = ValueAnimator.ofFloat(0f, mHookSize.toFloat()).apply {
duration = 650
addUpdateListener { anim ->
clipSize = anim.animatedValue as Float
invalidate()
}
}
AnimatorSet().apply {
playSequentially(toCircleAnim, translateAnim, clipAnim)
start()
}
}
如何控制动画顺序播放?
使用AnimatorSet动画集合里的playSequentially方法,即可顺序执行动画
AnimatorSet().apply {
playSequentially(toCircleAnim, translateAnim, clipAnim)
start()
}
}
动画的具体实现如下
首先创建一个类继承于View
class AnimationView : View {}
构造方法如下
constructor(context:Context):super(context){}
constructor(context:Context,attrs:AttributeSet?):super(context,attrs){}
然后添加绘制相关的工具和属性
/**
* 画笔
* */
private val mPaint by lazy {
Paint().apply {
color = resources.getColor(R.color.colorPrimary)
style = Paint.Style.FILL
}
}
/**
* 画笔,绘制小勾
* */
private val mHookPaint by lazy {
Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 5f
isAntiAlias = true
isDither = true
}
}
/**
* 绘制进度条背景
* */
private val mSliderPaint by lazy {
Paint().apply {
color = Color.MAGENTA
style = Paint.Style.FILL
}
}
实现回调函数
/**
* 回调函数
* */
var callBack:(() -> Unit)? = null
实现动画变化的接收值
/**
* 变成圆的动画音子
* */
private var mCornerRadius = 0f
/**
* 平移动画音子
* */
private var mTranValue = 0f
/**
* 进度的变化因子
* */
var progress = 0f
set(value) {
field = value
if (progress < 1.0f) {
//正在下载
invalidate()
} else {
//开启完成动画
startFinishAnimator()
}
}
注意,最后一个动画因子实现了set
方法,并且判断了该值是否达到1.0f
,达到了边开启下载完成动画,也是很好理解的,if
语句主要用来判断下载动画是否结束,如果progress
值小于1时,下载还未结束,如果等于1时,下载动画便结束,此时开启下自完成动画。
那么下载动画具体如何实现呢,这里实现滑动条缩小为一个圆的动画,但是没有实现显示圆时,成功需绘制一个小勾,失败需绘制一个小叉,后面会加进来,这里先不加
/**
* 下载完成动画
* */
private fun startFinishAnimator(){
val toCircleAnim = ValueAnimator.ofFloat(0f,height / 2f).apply {
duration = 1000
addUpdateListener {
mCornerRadius = it.animatedValue as Float
invalidate()
}
start()
}
val translateAnim = ValueAnimator.ofFloat(0f,(width - height) / 2f).apply {
duration = 1000
addUpdateListener {
mTranValue = it.animatedValue as Float
invalidate()
}
start()
}
}
然后实现点击事件,回调函数的调用,外部具体实现了函数调用时下载动画的开启
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN){
//点击事件回调
callBack?.let{back ->
back()
}
}
return true
}
最后执行具体绘制
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawRect(0f,0f,width.toFloat(),height.toFloat(),mSliderPaint)
canvas?.drawRoundRect(mTranValue,0f,progress * width - mTranValue,height.toFloat(),mCornerRadius,mCornerRadius,mPaint)
}
到这里其实还没结束,只绘制了滑动条缩放成一个圆,但动画结束时,还需要显示一个小勾或者小叉,所有还需要绘制一个小勾或者小叉,那么如何绘制最后显示的那个勾呢?
首先创建一个小勾路径Path,设置小勾的宽度为100,可以根据具体情况调整
//小勾固定大小
private val mHookSize = 100
//小勾路径
private var mPath = Path()
然后初始化控件中心坐标
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
cx = width / 2f
cy = height / 2f
}
那么如何知道需要绘制小勾或者小叉呢
这里维护了一个result
变量,用来判断下载结果是成功还是失败,默认成功
//下载结果
companion object {
//成功
const val failed = 0
//失败
const val success = 1
}
//下载成功
var result = success
如果下载失败,将变量设为failed
//假设下载失败
mProgressView.result = ProgressView.failed
然后判断result
值是成功还是失败绘制对应的小勾或者小叉
if (mTranValue == (width - height) / 2f) {
if (result == success) {
//成功画勾
mPath.apply {
moveTo(cx - mHookSize / 2, cy)
lineTo(cx - mHookSize / 4, cy + mHookSize / 2)
lineTo(cx + mHookSize / 2, cy - mHookSize / 4)
}
} else {
//失败画叉
mPath.apply {
moveTo(cx - mHookSize / 2, cy - mHookSize / 2)
lineTo(cx + mHookSize / 2, cy + mHookSize / 2)
moveTo(cx + mHookSize / 2, cy - mHookSize / 2)
lineTo(cx - mHookSize / 2, cy + mHookSize / 2)
}
}
canvas?.drawPath(mPath, mHookPaint)
canvas?.drawRect(
cx - mHookSize / 2 + clipSize, cy - mHookSize /
2, cx + mHookSize / 2 + 10, cy + mHookSize / 2 + 10,
mSliderPaint
)
}
注意,由于绘制小勾或小叉是最后绘制的,所有在执行完成动画后初始化对应的路径
将小勾绘制到屏幕上
canvas?.drawPath(mPath, mHookPaint)
最后是遮挡动画,用来遮挡绘制的小勾或小叉
//遮挡动画
val clipAnim = ValueAnimator.ofFloat(0f, mHookSize.toFloat()).apply {
duration = 650
addUpdateListener { anim ->
clipSize = anim.animatedValue as Float
invalidate()
}
}
AnimatorSet().apply {
playSequentially(toCircleAnim, translateAnim, clipAnim)
start()
}
最后加上小勾动态显示的动画
canvas?.drawRect(
cx - mHookSize / 2 + clipSize, cy - mHookSize /
2, cx + mHookSize / 2 + 10, cy + mHookSize / 2 + 10,
mSliderPaint
)
总结
如果看了这篇文章还是不太懂的同学,可以点击这里查看我的另一篇更简单的文章。
自己有的地方写的不好,还需不断改正