一款炫酷Loading动画--载入成功

简单介绍

昨天在简书上看到一篇文章。介绍了一个载入动画的实现过程
一款Loading动画的实现思路(一)
仅仅惋惜原动画是IOS上制做的。而看了一下。做者的实现思路比較复杂,因而趁着空暇写了一个Android版本号。这篇文章将给你们介绍一下实现过程。html


首先让咱们来看一下动画效果
这里写图片描写叙述


java

动画结构分析

从上面的gif图中可以看到,这个载入动画有成功,失败两种状态,由于Gif速度比較快,咱们再来分别看一张慢图android

一、成功状态载入动画

这里写图片描写叙述
成功动画的状态转移描写叙述例如如下:git

一、载入过程。画蓝色圆环,当进度为100%时,圆环完毕
二、从右側抛出蓝色小方块。小方块沿着曲线到达圆环正上方
三、蓝色小方块下落。下落过程当中,逐渐变长。当方块与圆圈接触时,进入圆环的部分变粗。同一时候圆环逐渐被挤压,变成椭圆形
四、方块底端到达圆环中心后,发出三个分叉向圆周延伸,同一时候椭圆被撑大。逐渐恢复回圆形
五、圆环变绿色画出绿色勾√github

整个过程可以说是比較复杂的,甚至对照原动画。事实上另外一些细节我没有去实现。只是接下来我为你们逐个分解每个过程是怎么实现的。而且并不难理解canvas

每个小过程组合起来,就是一款炫酷动画,但愿你们都有信心去了解它。markdown




本身定义View,依据进度绘制圆形

首先咱们来实现第一个过程。圆环的绘制。
在动画效果中。圆环的完整程度。是依据实际的进度来衡量的,当载入完毕。整个圆就画好了。
因此咱们本身定义一个View控件。在其提供了一个setProgress()方法来给使用者设置进度ide

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()方法里面。绘制圆弧
对于圆弧的绘制。相信你们都不会陌生(陌生也没有关系。由于很是easy),仅仅要调用一个canvas.drawArc()方法就可以了。
但是我要细致观察这里的圆形效果。在单独来看三张图post

圆弧起始状态

这里写图片描写叙述

圆弧运动状态

这里写图片描写叙述

圆弧终于状态

这里写图片描写叙述

可以看到,首先圆弧有必定的起始角度。咱们知道。在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);
    }



圆环完毕,抛出小方块

在圆环绘制完毕之后,会抛出一个小方块。小方块沿曲线运动到圆环正上方,实际整个曲线,是一段圆弧
咱们来看下图

方块运动状态

这里写图片描写叙述

运动状态分析图

这里写图片描写叙述
从图中可以看出,方块运动的终点,距离圆心为2R
若是运动轨迹是某个圆的一段弧,那么依据勾股定理有例如如下方程

(X+R)^2 + (2R)^2 = (X+2R)^2

解得X=R/2(事实上也很是easy解,就是勾三股四玄五)
若是咱们但愿方块在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);//蓝色圆环
    }



抛出完毕。方块下落

可以说下落过程,是整个动画中最复杂的过程了。包含方块下落。圆环挤压,方块变粗三个过程,整个过程,从方块下落開始,到方块底部究竟圆心
这里写图片描写叙述
首先是方块的下落,这个easy理解,方块会逐渐变长。由于在一样时间内,起始端和末端运动的距离不同
咱们拿末端做为样例,这里要使用到一个知识。就是P**ath路径类**
这是Android提供的一个类。表明咱们制定的一段路径实例。对于方块末端来讲,其运动的路径就是从顶部,到圆心

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大展身手

咱们首先来看,怎么初始化一个PathMeasure,很是easy,传入一个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计算出进入圆内的部分,和在圆外的部分了。
例如如下图:
这里写图片描写叙述
咱们知道,事实上圆/椭圆。都是依靠一个矩形肯定的。在这个动画中,咱们但愿圆被挤压成椭圆,终于缩放比例为0.8,大概是这种
这里写图片描写叙述
利用前面提到的计时器,咱们可以依据当前时间。知道圆被挤压的比例。实现挤压效果

//椭圆形区域
    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);
    }

因而咱们在必定时间内。逐渐得到勾这个路径的一部分。知道得到整个勾,并将其绘制出来!
终于效果例如如下:
这里写图片描写叙述


写在最后

本篇文章。首先介绍成功载入的动画实现过程下一篇文章将会接着介绍载入失败过程的实现。
经过这篇文章,咱们应该熟悉了Path,PathMeasure,Region等一系列API,利用这些API。咱们可以方便得绘制出路径效果。
每个步骤组合起来,就是一个好看的,复杂的动效。对于API不熟悉的朋友,建议用到的时候去查官方文档,或者看看其它朋友的一些介绍基础的文章。


最后,提供源代码下载地址github地址,欢迎你们下载和star

相关文章
相关标签/搜索