网上看到一个很炫的进度条动画,只可惜原动画是IOS上制作的,而看了一下,作者的实现思路比较复杂,于是趁着空闲写了一个Android版本,这篇文章将给大家介绍一下实现过程。 首先让我们来看一下动画效果
有了这个进度以后,我们就调用postInvalidate()去让控件重绘,其实就是触发了其ondraw()方法,然后我们就再ondraw()方法里面,绘制圆弧 对于圆弧的绘制,相信大家都不会陌生(陌生也没有关系,因为很简单),只要调用一个canvas.drawArc()方法就可以了。 但是我要仔细观察这里的圆形效果,在单独来看三张图
可以看到,首先圆弧有一定的起始角度,我们知道,在Android坐标系中,0度其实是指水平向右开始的 也就是起点的起始角度,其实是-90度,终点的起始角度,其实-150度
而整个过程中, 起点:-90度,逆时针旋转270度,最后回到0度位置 终点:-150度,与起点相差60度,最后相差360度,与起点重合
所以当progress=1,也就是动画完成时,起点会减去270度,那么对应每个progress 起点的位置应该是
每次获得新角度,我们就去重新绘制方块的位置:
那么问题来了,有了运动路径以后,我们希望有动画,起始就是希望,我们给定一个动画时间,我们可以获得在这段时间的某个点上,起始端/末端运动到路径的哪个位置 那么有了路径以后,我们能不能获得路径上的任意一个位置呢?答案是使用PathMeasure类。 可能有许多朋友对这个类不熟悉,可以参考一些文章,或者看看官方API介绍
我们首先来看,怎么初始化一个PathMeasure,很简单,传入一个Path对象即可,false表示不闭合这个路径
由于动画有一定时间,我们又需要一个计时器
接下来是使用PathMeasure获得下落过程中,起始端和末端的坐标
getPosTan()方法,第一个参数是指想要获得的路径长度,例如你设置的Path长度为100 那么你传入60,就会获得长度为60时的终点坐标(文字真的表达不好/(ㄒoㄒ)/~~,大家可以去看API)
根据起始端和末端的坐标*,我们绘制一条直线,就是小方块啦!
怎么区分圆内圆外呢?我决定自己判断太麻烦了,后来想到一个办法,判断交集! 我们知道,Android提供了API,让我们可以判断两个Rect是否相交,也可以获得它们的相交部分(也就是重合部分),还可以获得非重合部分。 假设我把方块看成是一个矩形,圆环看成一个矩形,那么问题就简单了,我就可以调用API计算出进入圆内的部分,和在圆外的部分了。 如下图:
这样,我们就有了代表椭圆的矩形。由于在一步中,我们知道了小方块的起始端和末端坐标,我们可以使这个两个坐标,分别向左右偏移一定距离,从而获得4个坐标,来创建矩形。 最后,我们直接利用两个矩形,取交集和非交集,具体实现如下:
Region是Android提供的,用于处理区域运算问题的一个类,使用这个类,我们可以很方便进行Rect交集补集等运算,不了解的朋友,查看API
最后绘制这两个区域,并且加上一个判断,就是这个两个矩形是否有相交,如果没有,那么圆环就不用被挤压,直接绘制圆环即可。
最后,还要记得将椭圆还原成圆,其实就是压缩的逆过程 效果如下:
最后将路径动态绘制出现,到这里大家都很熟悉这个做法了。但是这里我使用了另外一个方法,这个方法可以根据进度,直接返回当前路径成一个Path对象
下载地址:SuperLoadingProgress-master
最后说明一下,其实只要掌握了Canvas,只要你能想到的,都能实现。
动画结构分析
从上面的gif图中可以看到,这个加载动画有成功,失败两种状态,由于Gif速度比较快,我们再来分别看一张慢图1、成功状态加载动画
1、加载过程,画蓝色圆环,当进度为100%时,圆环完成 2、从右侧抛出蓝色小方块,小方块沿着曲线到达圆环正上方 3、蓝色小方块下落,下落过程中,逐渐变长,当方块与圆圈接触时,进入圆环的部分变粗,同时圆环逐渐被挤压,变成椭圆形 4、方块底端到达圆环中心后,发出三个分叉向圆周延伸,同时椭圆被撑大,逐渐恢复回圆形 5、圆环变绿色,画出绿色勾√整个过程可以说是比较复杂的,甚至对比原动画,其实还有一些细节我没有去实现,不过接下来我为大家逐个分解每个过程是怎么实现的,而且并不难理解。每个小过程组合起来,就是一款炫酷动画,希望大家都有信心去了解它。
自定义View,根据进度绘制圆形
首先我们来实现第一个过程,圆环的绘制。 在动画效果中,圆环的完整程度,是根据实际的进度来衡量的,当加载完成,整个圆就画好了。 所以我们自定义一个View控件,在其提供了一个setProgress()方法来给使用者设置进度public class SuperLoadingProgress extends View { /** * 当前进度 */ private int progress = 0; /** * 最大进度 */ private static final int maxProgress = 100; .... public void setProgress(int progress) { this.progress = Math.min(progress,maxProgress); postInvalidate(); if (progress==0){ status = 0; } } ... }
有了这个进度以后,我们就调用postInvalidate()去让控件重绘,其实就是触发了其ondraw()方法,然后我们就再ondraw()方法里面,绘制圆弧 对于圆弧的绘制,相信大家都不会陌生(陌生也没有关系,因为很简单),只要调用一个canvas.drawArc()方法就可以了。 但是我要仔细观察这里的圆形效果,在单独来看三张图
圆弧起始状态
圆弧运动状态
圆弧最终状态
可以看到,首先圆弧有一定的起始角度,我们知道,在Android坐标系中,0度其实是指水平向右开始的 也就是起点的起始角度,其实是-90度,终点的起始角度,其实-150度
而整个过程中, 起点:-90度,逆时针旋转270度,最后回到0度位置 终点:-150度,与起点相差60度,最后相差360度,与起点重合
所以当progress=1,也就是动画完成时,起点会减去270度,那么对应每个progress 起点的位置应该是
-90-270*progress当progress=1,终点和起点相差360度,而一开始就相差60度,所以整个过程就是多相差了300度,那么对应每个progress,终点和起点应该相差
-(60+precent*300)根据上面的结论,我们得到圆弧的具体绘制方式如下:
/** * 起始角度 */ private static final float startAngle = -90; @Override protected void onDraw(Canvas canvas) { ... float precent = 1.0f*progress/maxProgress;//当前完成百分比 //mRectF是代表整个view的范围 canvas.drawArc(mRectF, startAngle-270*precent, -(60 + precent*300), false, circlePaint); }
圆环完成,抛出小方块
在圆环绘制完成以后,会抛出一个小方块,小方块沿曲线运动到圆环正上方,实际整个曲线,是一段圆弧 我们来看下图方块运动状态
运动状态分析图
(X+R)^2 + (2R)^2 = (X+2R)^2解得X=R/2(其实也很容易解,就是勾三股四玄五) 假设我们希望方块在500ms内从起点运动到终点,那么我们就需要提供一个计时器,告诉我们现在运动了多少毫秒,然后根据这个时间,计算出方块当前位置 另外,由于方块本身有一定的长度,因此方块也有自己的起始端和末端。但是两者的运动轨迹是一样的,只是先后不同。
//抛出动画 endAngle = (float) Math.atan(4f/3); mRotateAnimation = ValueAnimator.ofFloat(0f, endAngle*0.9f ); mRotateAnimation.setDuration(500); mRotateAnimation.setInterpolator(new AccelerateDecelerateInterpolator()); mRotateAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { curSweepAngle = (float) animation.getAnimatedValue();//运动了多少角度 invalidate(); } });
每次获得新角度,我们就去重新绘制方块的位置:
/** * 抛出小方块 * @param canvas */ private void drawSmallRectFly(Canvas canvas){ canvas.save(); canvas.translate(radius / 2 + strokeWidth, 2 * radius + strokeWidth);//将坐标移动到大圆圆心 float bigRadius = 5*radius/2;//大圆半径 //方块起始端坐标 float x1 = (float) (bigRadius*Math.cos(curSweepAngle)); float y1 = -(float) (bigRadius*Math.sin(curSweepAngle)); //方块末端坐标 float x2 = (float) (bigRadius*Math.cos(curSweepAngle+0.05*endAngle+0.1*endAngle*(1-curSweepAngle/0.9*endAngle)));// float y2 = -(float) (bigRadius*Math.sin(curSweepAngle+0.05*endAngle+0.1*endAngle*(1-curSweepAngle/0.9*endAngle))); canvas.drawLine(x1, y1, x2, y2, smallRectPaint);//小方块,其实是一条直线 canvas.restore(); canvas.drawArc(mRectF, 0, 360, false, circlePaint);//蓝色圆环 }
抛出完成,方块下落
可以说下落过程,是整个动画中最复杂的过程了,包括方块下落,圆环挤压,方块变粗三个过程,整个过程,从方块下落开始,到方块底部到底圆心Path downPath1 = new Path();//起始端路径 downPath1.moveTo(2*radius+strokeWidth,strokeWidth); downPath1.lineTo(2 * radius+strokeWidth, radius+strokeWidth); Path downPath2 = new Path();//末端路径 downPath2.moveTo(2 * radius+strokeWidth, strokeWidth); downPath2.lineTo(2 * radius+strokeWidth, 2 * radius+strokeWidth);
那么问题来了,有了运动路径以后,我们希望有动画,起始就是希望,我们给定一个动画时间,我们可以获得在这段时间的某个点上,起始端/末端运动到路径的哪个位置 那么有了路径以后,我们能不能获得路径上的任意一个位置呢?答案是使用PathMeasure类。 可能有许多朋友对这个类不熟悉,可以参考一些文章,或者看看官方API介绍
我们首先来看,怎么初始化一个PathMeasure,很简单,传入一个Path对象即可,false表示不闭合这个路径
downPathMeasure1 = new PathMeasure(downPath1,false); downPathMeasure2 = new PathMeasure(downPath2,false);
由于动画有一定时间,我们又需要一个计时器
//下落动画 mDownAnimation = ValueAnimator.ofFloat(0f, 1f ); mDownAnimation.setDuration(500); mDownAnimation.setInterpolator(new AccelerateInterpolator()); mDownAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { downPrecent = (float) animation.getAnimatedValue(); invalidate(); } });
接下来是使用PathMeasure获得下落过程中,起始端和末端的坐标
//下落方块的起始端坐标 float pos1[] = new float[2]; float tan1[] = new float[2]; downPathMeasure1.getPosTan(downPrecent * downPathMeasure1.getLength(), pos1, tan1); //下落方块的末端坐标 float pos2[] = new float[2]; float tan2[] = new float[2]; downPathMeasure2.getPosTan(downPrecent * downPathMeasure2.getLength(), pos2, tan2);
getPosTan()方法,第一个参数是指想要获得的路径长度,例如你设置的Path长度为100 那么你传入60,就会获得长度为60时的终点坐标(文字真的表达不好/(ㄒoㄒ)/~~,大家可以去看API)
根据起始端和末端的坐标*,我们绘制一条直线,就是小方块啦!
方块下落,进入圆内部分变粗,圆被挤压变形
接下来要处理一个更加复杂的问题,就是进入圆环中的方块部分,要变粗。 为了解决这个问题,我们就需要分辨方块哪部分在圆内,哪部分在圆外,这个判断起来本身就很麻烦,况且,圆环还会被压缩!也就是园内圆外,没有一个固定的分界点!怎么区分圆内圆外呢?我决定自己判断太麻烦了,后来想到一个办法,判断交集! 我们知道,Android提供了API,让我们可以判断两个Rect是否相交,也可以获得它们的相交部分(也就是重合部分),还可以获得非重合部分。 假设我把方块看成是一个矩形,圆环看成一个矩形,那么问题就简单了,我就可以调用API计算出进入圆内的部分,和在圆外的部分了。 如下图:
//椭圆形区域 Rect mRect = new Rect(Math.round(mRectF.left),Math.round(mRectF.top+mRectF.height()*0.1f*downPrecent), Math.round(mRectF.right),Math.round(mRectF.bottom-mRectF.height()*0.1f*downPrecent));
这样,我们就有了代表椭圆的矩形。由于在一步中,我们知道了小方块的起始端和末端坐标,我们可以使这个两个坐标,分别向左右偏移一定距离,从而获得4个坐标,来创建矩形。 最后,我们直接利用两个矩形,取交集和非交集,具体实现如下:
//非交集 Region region1 = new Region(Math.round(pos1[0])-strokeWidth/4,Math.round(pos1[1]),Math.round(pos2[0]+strokeWidth/4),Math.round(pos2[1])); region1.op(mRect, Region.Op.DIFFERENCE); drawRegion(canvas, region1, downRectPaint); //交集 Region region2 = new Region(Math.round(pos1[0])-strokeWidth/2,Math.round(pos1[1]),Math.round(pos2[0]+strokeWidth/2),Math.round(pos2[1])); boolean isINTERSECT = region2.op(mRect, Region.Op.INTERSECT); drawRegion(canvas, region2, downRectPaint);
Region是Android提供的,用于处理区域运算问题的一个类,使用这个类,我们可以很方便进行Rect交集补集等运算,不了解的朋友,查看API
最后绘制这两个区域,并且加上一个判断,就是这个两个矩形是否有相交,如果没有,那么圆环就不用被挤压,直接绘制圆环即可。
//椭圆形区域 if(isINTERSECT) {//如果有交集 float extrusionPrecent = (pos2[1]-radius)/radius; RectF rectF = new RectF(mRectF.left, mRectF.top + mRectF.height() * 0.1f * extrusionPrecent, mRectF.right, mRectF.bottom - mRectF.height() * 0.1f * extrusionPrecent);//绘制椭圆 canvas.drawArc(rectF, 0, 360, false, circlePaint); }else{ canvas.drawArc(mRectF, 0, 360, false, circlePaint);//绘制圆 }
下落完成,绘制三叉
对于三叉的绘制,就没有什么特别的了,其实三叉就是三条Path路径,我们用类似前面的做法,利用一个计时器,三个Path,对应三个PathMeasure,就可以动态绘制出路径了。/** * 绘制分叉 * @param canvas */ private void drawFork(Canvas canvas) { float pos1[] = new float[2]; float tan1[] = new float[2]; forkPathMeasure1.getPosTan(forkPrecent * forkPathMeasure1.getLength(), pos1, tan1); float pos2[] = new float[2]; float tan2[] = new float[2]; forkPathMeasure2.getPosTan(forkPrecent * forkPathMeasure2.getLength(), pos2, tan2); float pos3[] = new float[2]; float tan3[] = new float[2]; forkPathMeasure3.getPosTan(forkPrecent * forkPathMeasure3.getLength(), pos3, tan3); canvas.drawLine(2 * radius+strokeWidth, radius+strokeWidth, 2 * radius+strokeWidth, 2 * radius+strokeWidth, downRectPaint); canvas.drawLine(2 * radius+strokeWidth, 2 * radius+strokeWidth, pos1[0], pos1[1], downRectPaint); canvas.drawLine(2 * radius+strokeWidth, 2 * radius+strokeWidth, pos2[0], pos2[1], downRectPaint); canvas.drawLine(2 * radius+strokeWidth, 2 * radius+strokeWidth, pos3[0], pos3[1], downRectPaint); //椭圆形区域 RectF rectF = new RectF(mRectF.left, mRectF.top + mRectF.height() * 0.1f * (1-forkPrecent), mRectF.right, mRectF.bottom - mRectF.height() * 0.1f * (1-forkPrecent)); canvas.drawArc(rectF, 0, 360, false, circlePaint); }
最后,还要记得将椭圆还原成圆,其实就是压缩的逆过程 效果如下:
绘制绿色勾√
绿色勾的绘制其实也和上面的做法类似,需要一个计时器,一个Path,对应的PathMeasure即可 勾的路径如下://初始化打钩路径 Path tickPath = new Path(); tickPath.moveTo(1.5f * radius+strokeWidth, 2 * radius+strokeWidth); tickPath.lineTo(1.5f * radius + 0.3f * radius+strokeWidth, 2 * radius + 0.3f * radius+strokeWidth); tickPath.lineTo(2*radius+0.5f * radius+strokeWidth,2*radius-0.3f * radius+strokeWidth); tickPathMeasure = new PathMeasure(tickPath,false);
最后将路径动态绘制出现,到这里大家都很熟悉这个做法了。但是这里我使用了另外一个方法,这个方法可以根据进度,直接返回当前路径成一个Path对象
/** * 绘制打钩 * @param canvas */ private void drawTick(Canvas canvas) { Path path = new Path(); /* * On KITKAT and earlier releases, the resulting path may not display on a hardware-accelerated Canvas. * A simple workaround is to add a single operation to this path, such as dst.rLineTo(0, 0). */ tickPathMeasure.getSegment(0, tickPrecent * tickPathMeasure.getLength(), path, true);//该方法,可以获得整个路径的一部分 path.rLineTo(0, 0);//解决Android本身的一个bug canvas.drawPath(path, tickPaint);//绘制出这一部分 canvas.drawArc(mRectF, 0, 360, false, tickPaint); }
下载地址:SuperLoadingProgress-master
最后说明一下,其实只要掌握了Canvas,只要你能想到的,都能实现。
收藏的用户(0) X
正在加载信息~
推荐阅读
最新回复 (0)
站点信息
- 文章2300
- 用户1336
- 访客10861715
每日一句
True success inspires others to act.
真正的成功是激励他人行动。
真正的成功是激励他人行动。
语法错误: 意外的令牌“标识符”
全面理解Gradle - 定义Task
Motrix全能下载工具 (支持 BT / 磁力链 / 百度网盘)
谷歌Pixel正在开始起飞?
获取ElementUI Table排序后的数据
Run-Time Check Failure #0 - The value of ESP was not properly saved across a function call. This is
亲测!虚拟机VirtualBox安装MAC OS 10.12图文教程
华为手机app闪退重启界面清空log日志问题
android ndk开发之asm/page.h: not found
手机屏幕碎了怎么备份操作?
免ROOT实现模拟点击任意位置
新手必看修改DSDT教程
thinkpad t470p装黑苹果系统10.13.2
新会员