Android仿支付宝笑脸刷新加载动画的实现代码,加载成功

看到支付宝的下拉刷新有一个笑脸的动画,因此自己也动手实现一下。效果图如下:

简介

昨天在简书上看到一篇文章,介绍了一个加载动画的实现过程
一款Loading动画的实现思路(一)
只可惜原动画是IOS上制作的,而看了一下,作者的实现思路比较复杂,于是趁着空闲写了一个Android版本,这篇文章将给大家介绍一下实现过程。
首先让我们来看一下动画效果

冠亚体育手机网站 1

动画效果

冠亚体育手机网站 2

动画结构分析

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

一、总体思路

1、成功状态加载动画

冠亚体育手机网站 3

成功状态加载动画

成功动画的状态转移描述如下:

冠亚体育手机网站,1、加载过程,画蓝色圆环,当进度为100%时,圆环完成
2、从右侧抛出蓝色小方块,小方块沿着曲线到达圆环正上方
3、蓝色小方块下落,下落过程中,逐渐变长,当方块与圆圈接触时,进入圆环的部分变粗,同时圆环逐渐被挤压,变成椭圆形
4、方块底端到达圆环中心后,发出三个分叉向圆周延伸,同时椭圆被撑大,逐渐恢复回圆形
5、圆环变绿色画出绿色勾√

整个过程可以说是比较复杂的,甚至对比原动画,其实还有一些细节我没有去实现,不过接下来我为大家逐个分解每个过程是怎么实现的,而且并不难理解每个小过程组合起来,就是一款炫酷动画,希望大家都有信心去了解它。

1、静态部分的笑脸。

自定义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()方法就可以了。
但是我要仔细观察这里的圆形效果,在单独来看三张图

这一部分的笑脸就是一个半圆弧,加上两颗眼睛,这部分比较简单,用于一开始的展示。

圆弧起始状态

冠亚体育手机网站 4

圆弧起始状态

2、动态笑脸的实现。

圆弧运动状态

冠亚体育手机网站 5

圆弧运动状态

2.1、先是从底部有一个圆形在运动,运动在左眼位置时把左眼给绘制,同时圆形继续运动,运动到右眼位置时绘制右眼,圆形继续运动到最右边的位置。

圆弧最终状态

冠亚体育手机网站 6

圆弧最终状态

可以看到,首先圆弧有一定的起始角度,我们知道,在Android坐标系中,0度其实是指水平向右开始的
也就是起点的起始角度,其实是-90度终点的起始角度,其实-150度

而整个过程中,
起点:-90度,逆时针旋转270度,最后回到0度位置
终点:-150度,与起点相差60度,最后相差360度,与起点重合

所以当progress=1,也就是动画完成时,起点会减去270度,那么对应每个progress
起点的位置应该是

*-90-270progress\

当progress=1,终点和起点相差360度,而一开始就相差60度,所以整个过程就是多相差了300度,那么对应每个progress,终点和起点应该相差

*-(60+precent300)\

根据上面的结论,我们得到圆弧的具体绘制方式如下:

    /**
    * 起始角度
    */
    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);
    }

2.2、当上面的圆形运动到最右边时候,开始不断绘制脸,从右向左,脸不断增长,这里脸设置为接近半个圆形的大小。

圆环完成,抛出小方块

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

2.3、当脸画完的时候,开始让脸旋转起来,就是一边在增长的同时,另一边是在缩短的。

方块运动状态

冠亚体育手机网站 7

方块运动状态

2.4、最后脸的部分是慢慢缩为一个点的,此时动画结束。

运动状态分析图

冠亚体育手机网站 8

运动状态分析图

从图中可以看出,方块运动的终点,距离圆心为2R
假设运动轨迹是某个圆的一段弧,那么根据勾股定理有如下方程

(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);//蓝色圆环
    }

2.5、时间可以看做时最底部的那个圆形运动了两周,因此可以用分数来表示每一部分的运动,如从底部开始到左眼睛的位置,用时比例为(1/4+1/8),最终控制每一部分的动画比例的和加起来为2即可。

抛出完成,方块下落

可以说下落过程,是整个动画中最复杂的过程了,包括方块下落,圆环挤压,方块变粗三个过程,整个过程,从方块下落开始,到方块底部到底圆心

冠亚体育手机网站 9

抛出完成,方块下落

首先是方块的下落,这个容易理解,方块会逐渐变长,因为在相同时间内,起始端和末端运动的距离不一样
我们拿末端作为例子,这里要使用到一个知识,就是Path路径类
这是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,很简单,传入一个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)

根据起始端和末端的坐标,我们绘制一条直线*,就是小方块啦!

大概是这样的时间比例:(1/4+1/8) + (1/4) +(1/8) +(1/2) +(1/4) +(1/4+1/4)
,其中1/4代表1/4个圆弧,也是1/4的时间,其它的类似。

方块下落,进入圆内部分变粗,圆被挤压变形

接下来要处理一个更加复杂的问题,就是进入圆环中的方块部分,要变粗
为了解决这个问题,我们就需要分辨方块哪部分在圆内,哪部分在圆外,这个判断起来本身就很麻烦,况且,圆环还会被压缩!也就是园内圆外,没有一个固定的分界点!

怎么区分圆内圆外呢?我决定自己判断太麻烦了,后来想到一个办法,判断交集!
我们知道,Android提供了API,让我们可以判断两个Rect是否相交,也可以获得它们的相交部分(也就是重合部分),还可以获得非重合部分。
假设我把方块看成是一个矩形,圆环看成一个矩形,那么问题就简单了,我就可以调用API计算出进入圆内的部分,和在圆外的部分了。
如下图:

冠亚体育手机网站 10

判断交集

我们知道,其实圆/椭圆,都是依靠一个矩形确定的,在这个动画中,我们希望圆被挤压成椭圆,最终缩放比例为0.8,大概是这样的

冠亚体育手机网站 11

圆/椭圆

利用前面提到的计时器,我们可以根据当前时间,知道圆被挤压的比例,实现挤压效果

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

最后,还要记得将椭圆还原成圆,其实就是压缩的逆过程
效果如下:

冠亚体育手机网站 12

下落完成,绘制三叉

1、重写onMeasure()方法

绘制绿色勾√

绿色勾的绘制其实也和上面的做法类似,需要一个计时器,一个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);
    }

于是我们在一定时间内,逐渐获得勾这个路径的一部分,知道获得整个勾,并将其绘制出来!
最终效果如下:

冠亚体育手机网站 13

画勾

处理为wrap_content情况,那么它的specMode是AT_MOST模式,在这种模式下它的宽/高等于spectSize,这种情况下view的spectSize是parentSize,而parentSize是父容器目前可以使用大小,就是父容器当前剩余的空间大小,
就相当于使用match_parent一样 的效果,因此我们可以设置一个默认的值。

写在最后

本篇文章,首先介绍成功加载的动画实现过程下一篇文章将会接着介绍加载失败过程的实现。
通过这篇文章,我们应该熟悉了Path,PathMeasure,Region等一系列API,利用这些API,我们可以方便得绘制出路径效果。
每个步骤组合起来,就是一个好看的,复杂的动效。对于API不熟悉的朋友,建议用到的时候去查官方文档,或者看看其他朋友的一些介绍基础的文章。
最后,提供源码下载地址和github地址,欢迎大家下载和star

未经授权,谢绝转载。
更多文章,可以关注我的csdn博客,一起学习。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpectMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpectSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpectMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpectSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpectMode == MeasureSpec.AT_MOST
&& heightSpectMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpectMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpectSize);
} else if (heightSpectMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpectSize, mHeight);
}
}

2、在构造函数中调用init()方法

进行初始化,之所以看到运动中圆弧能够在右边增长的同时,左边的也在减少是使用PathMeasure类中的getSegment方法来截取任意一段长度的路径。

private void init(Context context, AttributeSet attrs) {
drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG
| Paint.FILTER_BITMAP_FLAG);
lineWidth = dip2px(context, lineWidth);
radius = dip2px(context, radius);
path = new Path();
pathCircle = new Path();
pathCircle2 = new Path();
//在path中添加一个顺时针的圆,这时候路径的起点和终点在最后边
//在画前半部分的脸和运动中的脸,起点在最右边比较方便的计算,但在最后那部分,运动的终点
//是在圆形的底部,这样把路径圆进行转换到底部,方便计算
pathCircle.addCircle(0, 0, radius, Direction.CW);
pathCircle2.addCircle(0, 0, radius, Direction.CW);
//利用Matrix,让pathCircle中的圆旋转90度,这样它的路径的起点和终点都在底部了
Matrix m = new Matrix();
m.postRotate(90);
pathCircle.transform(m);
//画脸的笔
paint = new Paint();
//画眼睛的笔
eyePaint = new Paint();
paint.setColor(blue);
eyePaint.setColor(blue);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineWidth);
eyePaint.setStrokeWidth(lineWidth);
//设置画脸的笔的端点为圆角(即起点和终点都是圆角)
paint.setStrokeCap(Paint.Cap.ROUND);
//使用PathMeasure计算路径的信息
pm = new PathMeasure();
pm.setPath(pathCircle, false);
pm2 = new PathMeasure();
pm2.setPath(pathCircle2, false);
//路径的长度,两个路径都是圆形,因此只计算其中一个即可
length = pm.getLength();
eyeRadius = (float)(lineWidth/2.0+lineWidth/5.0);
}

3、画静态笑脸

pm2.getSegment()方法可以获取指定长度的路径,然后保存在path中,在第二步已经把一个圆加到path中去,并初始化了pm2了。

/**静态的笑脸
* @param canvas
*/
private void first(Canvas canvas) {
pm2.getSegment(10, length / 2-10, path, true);
canvas.drawPath(path, paint);
path = new Path();
drawEye(canvas);
}
/**一开始画的眼睛
* @param canvas
*/
public void drawEye(Canvas canvas) {
float x = (float) ((radius) * Math.cos(Math.PI * 45 / 180));
float y = x;
canvas.drawCircle(-x, -y, eyeRadius , eyePaint);
canvas.drawCircle(x, -y, eyeRadius , eyePaint);
}

4、实现运动的圆形的方法,即动画开始部分。

这里记得要进行角度转换,π=180

/**从底部开始画一个在运动的圆,运动时间为0-3/4
* 即从270度开始,逆时针到0度
* @param canvas
*/
private void drawCircle(Canvas canvas) {
float degree = 270 - 270 * 4 / 3 * fraction;
float x = (float) ((radius ) * Math.cos(Math.PI * degree/180));
float y = -(float) ((radius ) * Math.sin(Math.PI * degree/ 180));
canvas.drawCircle(x, y, eyeRadius, eyePaint);
}

5、在圆形运动的同时画眼睛

在圆形运动到左眼的位置时,同时绘制左眼,时间为1/4+1/8=3/8
运动到右眼位置时绘制右眼,时间为1/4+1/8+1/4=5/8
两个眼睛的位置都设为45度

/* @param canvas
* @param pos 0代表画左眼,1代表画右眼
*/
public void drawOneEye(Canvas canvas, int pos) {
float x = (float) ((radius) * Math.cos(Math.PI * 45 / 180));
float y = x;
if (pos == 0) {
canvas.drawCircle(-x, -y, eyeRadius, eyePaint);
}else if(pos==1){
canvas.drawCircle(x, -y, eyeRadius , eyePaint);
}
}

6、动画进行之后绘制笑脸

笑脸部分是半个圆,因此截取的最大长度是length/2
用的时间是1/2,画完之后fraction应该到了5/4的时间了,1/4+1/8+1/4+1/8+1/2=5/4

public void drawFace(Canvas canvas){
//需要重新给path赋值
path=null;
path = new Path();
//根据时间来截取一定长度的路径,保存到path中,取值范围(0,length/2)
pm2.getSegment(0, (float) (length*(fraction-0.75)), path, true);
canvas.drawPath(path, paint);
}

7、笑脸绘制完成后,把它动起来。

把圆脸部分逆时针旋转起来,截取最大长度还是length/2,
用的是这个方法pm2.getSegment(),运动的时间为1/4时间,需要不断改变起点和终点,这样圆脸才会动起来

public void rotateFace(Canvas canvas){
path=null;
path = new Path();
pm2.getSegment((float)(length*(fraction-5.0/4)), (float)(length*(fraction-5.0/4)+length*0.5), path, true);
canvas.drawPath(path, paint);
}

8、最后那部分动画的实现。

剩下的1/4时间,就用另外一个PathMeasure,这个圆的路径起点是底部的,在初始化时候已经进行转换,因为这样设置比较方便的计算它的终点位置。

public void drawLastFact(Canvas canvas){
path = null;
path = new Path();
//从起点的1/4长度开始(即最左边的圆点),到圆的路径的终点(即底部)
pm.getSegment((float)(1.0/4*length+3.0/2*(fraction-3.0/2)*length), (float)(length/2.0+length/8.0+(fraction-3.0/2)*length), path, true);
canvas.drawPath(path, paint);
}

9、属性动画的实现

public void performAnim() {
//上面计算的时间比例,加起来就是2,是运动了两周,因此这里设置为(0,2)
val = ValueAnimator.ofFloat(0, 2);
val.setDuration(duration);
val.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator arg0) {
fraction = (float) arg0.getAnimatedValue();
postInvalidate();
}
});
val.setRepeatCount(repeaCount);
val.start();
val.setRepeatMode(ValueAnimator.RESTART);
}

10 、在onDraw()方法中调用一上方法。

这里的fraction的范围是[0,2],每个片段就用分数表示,最后它们的和刚好是2。

@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
if (changed) {
mWidth = right - left;
mHeight = bottom - top;
}
}
}
@Override
protected void onDraw(Canvas canvas) {
//从画布上去除锯齿
canvas.setDrawFilter(drawFilter);
canvas.translate(mWidth / 2, mHeight / 2);
if (fraction == -1||!val.isRunning())
first(canvas);
//从底部开始画一个在运动的圆,运动时间为0-3/4
if (0 < fraction && fraction < 0.75) {
drawCircle(canvas);
}
//画左眼
if (fraction > 1.0 * 3 / 8&&fraction<1.0*6/4) {
drawOneEye(canvas,0);
}
//画右眼
if(fraction>1.0*5/8&&fraction<1.0*6/4){
drawOneEye(canvas, 1);
}
//画脸
if(fraction>=0.75&&fraction<=5.0/4){
drawFace(canvas);
}
//把脸运动起来
if(fraction>=5.0/4&&fraction<=(5.0/4+1.0/4)){
rotateFace(canvas);
}
if(fraction>=6.0/4){
drawLastFact(canvas);
}
}

11、其它的方法和字段的定义

/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
//字段
private final int blue = 0xff4aadff;
private int mWidth = 200;
private int mHeight = 200;
private int radius = 20;
private int lineWidth = 5;
private float eyeRadius;
Paint paint,eyePaint;
DrawFilter drawFilter;
Path path, pathCircle,pathCircle2;
PathMeasure pm,pm2;
float length;// 圆周长
float fraction = -1;
long duration = 2000;
int repeaCount = 8;
float x = 0, y = 0;
ValueAnimator val;

11、自定义控件的使用

//在布局中的设置
<com.example.test22.view.SmileView 
android:id="@+id/smile"
android:layout_width="match_parent"
android:layout_height="100dp"/>

在Activity中,

SmileView smile;
smile = (SmileView)findViewById(R.id.smile);
//设置动画执行时间,重复的次数。
smile.setDuration(2000);
smile.setRepeaCount(8);
//执行动画
smile.performAnim();
//停止动画
smile.cancelAnim();

三、总结

我觉得难点在于运动中圆弧的一边增长的同时,另一边在缩短的控制,计算的不好就很容易出现从一个片段到另外一个片段时候跳跃十分明显,在这里我用到两个路径的圆,一个圆的起点在最右边,一个圆起点在底部,这样在处理最后那部分,片段的终点需要回到底部时候比较方便。重点在于PathMeasure类的getSegment()方法的运用,同时会改变默认路径的起点。

以上所述是小编给大家介绍的Android仿支付宝笑脸刷新加载动画的实现代码,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!

您可能感兴趣的文章:

  • Android使用glide加载gif动画设置播放次数
  • Android
    Glide图片加载(加载监听、加载动画)
  • Android自定义加载loading
    view动画组件
  • Android实现跳动的小球加载动画效果
  • Android加载Gif动画实现代码
  • Android实现仿慕课网下拉加载动画
  • Android自定义加载控件实现数据加载动画
  • Android自定义view实现阻尼效果的加载动画
  • Android自定义View实现loading动画加载效果
  • Android自定义加载圈动画效果

Post Author: admin

发表评论

电子邮件地址不会被公开。 必填项已用*标注