首页 > 代码库 > 仿支付宝笑脸刷新加载动画的实现

仿支付宝笑脸刷新加载动画的实现

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

一、总体思路

1、静态部分的笑脸。

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

2、动态笑脸的实现。

2.1、先是从底部有一个圆形在运动,运动在左眼位置时把左眼给绘制,同时圆形继续运动,运动到右眼位置时绘制右眼,圆形继续运动到最右边的位置。
2.2、当上面的圆形运动到最右边时候,开始不断绘制脸,从右向左,脸不断增长,这里脸设置为接近半个圆形的大小。
2.3、当脸画完的时候,开始让脸旋转起来,就是一边在增长的同时,另一边是在缩短的。
2.4、最后脸的部分是慢慢缩为一个点的,此时动画结束。
2.5、时间可以看做时最底部的那个圆形运动了两周,因此可以用分数来表示每一部分的运动,如从底部开始到左眼睛的位置,用时比例为(1/4+1/8),最终控制每一部分的动画比例的和加起来为2即可。
大概是这样的时间比例:(1/4+1/8) + (1/4) +(1/8) +(1/2) +(1/4) +(1/4+1/4) ,其中1/4代表1/4个圆弧,也是1/4的时间,其它的类似。

二、代码实现

1、重写onMeasure()方法

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

@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()方法的运用,同时会改变默认路径的起点。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    仿支付宝笑脸刷新加载动画的实现