加载动图的实现及属性动画的使用

昨天在一个APP上面看见一个吃东西的加载图,感觉挺简单的,于是打算去实现。但是在实现的过程中踩了一些坑,那个图也不能找到做参考了,于是自己琢磨了一个下午,终于实现了效果。因此这里将整个过程做一个复盘,以便自己能深刻的记住相关知识点。
本来应该像大佬一样将图片效果、引用方式、各个参数写出来,但是鉴于我比较懒,不对,是没有bintray.com的账号。因此就将整个过程以及源码(就一个类,有需要的朋友可以增加自定义属性,供以后使用),先来一张效果图镇店:

效果图

首先是布局,我是这样构思的:

布局

将整个界面左右留下100px的内边距,然后将界面一分为二,左边是一个嘴的圆形、右边是三个圆球(Food),接下来这里就比较容易了,就是确定尺寸,绘制扇形。

自定义View确定尺寸一般是onMeasure方法,在开发者艺术探索中曾看到这样的描述:

measure过程中决定了View的宽/高,Measure完成以后,可以通过getMeasureWidth和getMeasureHeight方法来获取到View测量后的宽/高,在几乎所有的情况下它都等同于View最终的宽高。

但是我看到很多自定义View都没有重写这个方法,而是通过重写onSizeChanged获得整个View的宽高,原因是像我们实现的这种自定义控件一般情况不需要使用到Padding,因此我们不会通过onMeasure来获取,因为这个方法获取相对而言复杂一些,而onSizeChanged直接将值传过了,不需要进行任何处理就能获得View的宽高。


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //View的宽度
        this.mWidth = w;
        //View的高度
        this.mHeight = h;
        //圆形半径
        int raduis = (mWidth/2-100)/2;
        //绘制扇形的矩形
        mRectF.set(-raduis,-raduis,raduis,raduis);

    }

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(mRectF.right+50, mHeight / 2);
        canvas.drawArc(mRectF, 30, 300, true, paint);
    }

这里我当时有一个疑问是绘制扇形的时候,我当时想的是,我绘制的角度是30°到330°,因此drawArc方法的起始角度应该是这样:

canvas.drawArc(mRectF, 30, 330, true, paint);

结果绘制出来的圆形嘴的角度只有30°,后来测试了几个角度才明白,它这里的起始角度的确是起点的角度,但是重点角度不是圆形的角度,而是你绘制的角度。这里有点绕,就是说终点角度指的是你需要绘制的角度是多少度,我需要露出60度的嘴,因此我绘制的角度应该是300度,不是330度。而330度的话,剩下30度的嘴也是正常的。因此如果想要绘制逆时针的扇形的话,可以尝试终点角度为负值,如下:

canvas.drawArc(mRectF, 150, -300, true, paint);

150度起点
关于绘制扇形角度的问题上面描述得并不准确,比如当我们绘制的起点角度为30度时,则图形又变得不能理解了:

canvas.drawArc(mRectF, 30, -300, true, paint);

30度起点

不管如何,我们可以实现效果就好,如果需要做扇形统计图之类的动态变化的话,则需要深入了解起始角度的真实意义,我查看源码没有发现处理起始角度是具体数据:

private static native void native_drawArc(long nativeCanvas, float left, float top,
float right, float bottom,
float startAngle, float sweep, boolean useCenter,
long nativePaint);

drawArc
void drawArc (RectF oval,
float startAngle,
float sweepAngle,
boolean useCenter,
Paint paint)

Draw the specified arc, which will be scaled to fit inside the specified oval.

If the start angle is negative or >= 360, the start angle is treated as start angle modulo 360.

If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is negative, the sweep angle is treated as sweep angle modulo 360

The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0 degrees (3 o’clock on a watch.)

Parameters
oval RectF: The bounds of oval used to define the shape and size of the arc
This value must never be null.
startAngle float: Starting angle (in degrees) where the arc begins
sweepAngle float: Sweep angle (in degrees) measured clockwise
useCenter boolean: If true, include the center of the oval in the arc, and close it if it is being stroked. This will draw a wedge
paint Paint: The paint used to draw the arc
This value must never be null.
绘制指定的弧,其将被缩放以适合指定的椭圆。

如果起始角度为负或> = 360,起始角度被视为起始角度模数360。

如果扫掠角度> 360°,则椭圆形被完全绘制。 请注意,这与SkPath :: arcTo略有不同,它将扫描角度视为360度。如果扫描角度为负,则扫描角度被视为360度扫描角度

弧线顺时针拉伸。 0度的角度对应于0度(手表3点钟)的几何角度。

绘制好了圆形的嘴,接下来我们可以考虑圆球的布局了,我是这样构思的:

布局

小球在2/3的圆球直径的三个点依次排开,代码如下:

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.mWidth = w;
        this.mHeight = h;
        //圆形半径
        int raduis = (mWidth/2-100)/2;
        mRectF.set(-raduis,-raduis,raduis,raduis);
        //小球位置1
        centerX1 = raduis+2f/3*raduis-raduis*1/5f;
        //小球位置2
        centerX2 = raduis+4f/3*raduis-raduis*1/5f;
        //小球位置3
        centerX3 = raduis+6/3*raduis-raduis*1/5f;

    }


  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(mRectF.right+100, mHeight / 2);
        canvas.drawArc(mRectF, 150, -300, true, paint);
        paint.setColor(Color.GREEN);
        canvas.drawCircle(centerX1,0,mRectF.right*0.2f,paint);
        paint.setColor(Color.BLUE);
        canvas.drawCircle(centerX2,0,mRectF.right*0.2f,paint);
        paint.setColor(Color.YELLOW);
        canvas.drawCircle(centerX3,0,mRectF.right*0.2f,paint);
    }

逼叨叨了这么久,终于把界面展示出来了,那么我们这个动画如何实现呢?网上关于属性动画的文章非常多,我觉得这个文章讲得足够详细了—— Android属性动画完全解析!而且我们这里的动画根本用不上动画集合,一个动画就足够,我的思路是在做一个动画,动画中小球的移动路径为X放向大圆球直径的2/3,当移动了90%的时候,最前面的小球隐藏,打球闭合,动画结束之后再次执行,将三个小球的初始位置依次往前一格提一步,就可以看到类似吃掉的效果了!
描述起来比较抽象,咱们再看看效果图:

这里写图片描述

然后我们可以看看动画与绘制方法:

   public void startMoving(){
        if(getVisibility() != View.VISIBLE || isMoving){
            return;
        }
        isMoving = true;
        Moving = true;

        anim = ValueAnimator.ofFloat(0f,1f);
        anim.setDuration(2000);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentValue = (float) animation.getAnimatedValue();

                dex = currentValue*mRectF.right*2f/3;

                if(currentValue>0.9f ){
                    //嘴闭合
                    isClose = true;
                }else{
                    //嘴张开
                    isClose = false;
                }

                postInvalidate();
            }


        });
        anim.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                isClose = false;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                isMoving = false;

                if(Moving){
                    //动态切换小球位置,形成流式滚动
                    float dest = centerX1;
                    centerX1 = centerX3;
                    centerX3 = centerX2;
                    centerX2 = dest;
                    startMoving();
                }

            }

            @Override
            public void onAnimationCancel(Animator animation) {
                //当动画停止时小球位置回归原点
                centerX1 = mRectF.right+2f/3*mRectF.right-mRectF.right*1/5f;
                centerX2 = mRectF.right+4f/3*mRectF.right-mRectF.right*1/5f;
                centerX3 = mRectF.right+6/3*mRectF.right-mRectF.right*1/5f;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        //保证小球匀速滚动
        anim.setInterpolator(new LinearInterpolator());
        anim.start();
    }


public void stopMoving(){
        Moving = false;
        anim.cancel();
    }


 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        paint.setColor(Color.RED);
        canvas.translate(mRectF.right+100, mHeight / 2);


        if(isClose){
            canvas.drawArc(mRectF, 0, 360, true, paint);
            float checkSize = 22f/15*mRectF.right;

            //将在第一个展示的小球不予绘制
            if(centerX1-dex>checkSize){
                paint.setColor(Color.GREEN);
                canvas.drawCircle(centerX1-dex,0,mRectF.right*0.2f,paint);

            }
            if(centerX2-dex>checkSize){
                paint.setColor(Color.BLUE);
                canvas.drawCircle(centerX2-dex,0,mRectF.right*0.2f,paint);

            }
            if(centerX3-dex>checkSize){
                paint.setColor(Color.YELLOW);
                canvas.drawCircle(centerX3-dex,0,mRectF.right*0.2f,paint);
            }

        }else{
            canvas.drawArc(mRectF, 30, 300, true, paint);
            paint.setColor(Color.GREEN);
            canvas.drawCircle(centerX1-dex,0,mRectF.right*0.2f,paint);
            paint.setColor(Color.BLUE);
            canvas.drawCircle(centerX2-dex,0,mRectF.right*0.2f,paint);
            paint.setColor(Color.YELLOW);
            canvas.drawCircle(centerX3-dex,0,mRectF.right*0.2f,paint);
        }


    }

我之前在动画的实现过程中有一个坑,我在处理小球位置的时候,addUpdateListener接口的onAnimationUpdate方法里面直接处理用小球的X坐标减去动画的更新值,如下:

 @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentValue = (float) animation.getAnimatedValue();

                dex = currentValue*mRectF.right*2f/3;
                centerX3-=dex;
                centerX2-=dex;
                centerX1-=dex;
                if(currentValue>0.9f ){
                    isClose = true;
                }else{
                    isClose = false;
                }

                postInvalidate();
            }

结果在动画过程中后面的速度不断加快,导致不能得到预期的效果,后来一步一步检查才发现,这个属性值没有处理合适。

如果我们这个空间需要在横竖屏都能使用的话,还需要对宽高进行一个判断,修改如下:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.mWidth = w;
        this.mHeight = h;
        int normal = (w<h)?w:h;

        //圆形半径
        float raduis = (normal/2-Padding)/2;
        mRectF.set(-raduis,-raduis,raduis,raduis);
        //小球位置1
        centerX1 = raduis+2f/3*raduis-raduis*1/5f;
        centerX2 = raduis+4f/3*raduis-raduis*1/5f;
        centerX3 = raduis+6/3*raduis-raduis*1/5f;

    }

如果需要封装为一个自定义控件,方便今后随时能用的话,设置自定义属性的时候可以设置View的padding属性,然后通过onMeasure获取,或者通过自定义属性获取,还有颜色、小球半径(建议小于1/3大球半径)等。
这个View就一个java文件,因此就不贴代码连接了,直接显示出来:

public class EatLoading extends View {
    private static final float Padding = 100;
    private int mWidth;
    private int mHeight;
    private Paint paint;    //画笔
    private RectF mRectF;    //矩形
    private boolean isMoving = false;//当前动画是否结束
    private boolean isClose = false;//圆形是否闭合
    private boolean Moving = false;//循环动画
    private float centerX1,centerX2,centerX3;
    private float dex = 0;
    private ValueAnimator anim;

    public EatLoading(Context context) {
        super(context);
        initPaint();
    }

    public EatLoading(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public EatLoading(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }
    //初始化画笔
    private void initPaint() {
        paint = new Paint();
        //设置画笔模式:填充
        paint.setStyle(Paint.Style.FILL);
        paint.setTextSize(30);
        //初始化区域
        mRectF = new RectF();
    }



    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.mWidth = w;
        this.mHeight = h;
        int normal = (w<h)?w:h;

        //圆形半径
        float raduis = (normal/2-Padding)/2;
        mRectF.set(-raduis,-raduis,raduis,raduis);
        //小球位置1
        centerX1 = raduis+2f/3*raduis-raduis*1/5f;
        centerX2 = raduis+4f/3*raduis-raduis*1/5f;
        centerX3 = raduis+6/3*raduis-raduis*1/5f;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        paint.setColor(Color.RED);
        canvas.translate(mRectF.right+Padding, mHeight / 2);


        if(isClose){
            canvas.drawArc(mRectF, 0, 360, true, paint);
            float checkSize = 22f/15*mRectF.right;
            if(centerX1-dex>checkSize){
                paint.setColor(Color.GREEN);
                canvas.drawCircle(centerX1-dex,0,mRectF.right*0.2f,paint);

            }
            if(centerX2-dex>checkSize){
                paint.setColor(Color.BLUE);
                canvas.drawCircle(centerX2-dex,0,mRectF.right*0.2f,paint);

            }
            if(centerX3-dex>checkSize){
                paint.setColor(Color.YELLOW);
                canvas.drawCircle(centerX3-dex,0,mRectF.right*0.2f,paint);
            }

        }else{
            canvas.drawArc(mRectF, 30, 300, true, paint);
            paint.setColor(Color.GREEN);
            canvas.drawCircle(centerX1-dex,0,mRectF.right*0.2f,paint);
            paint.setColor(Color.BLUE);
            canvas.drawCircle(centerX2-dex,0,mRectF.right*0.2f,paint);
            paint.setColor(Color.YELLOW);
            canvas.drawCircle(centerX3-dex,0,mRectF.right*0.2f,paint);
        }


    }

    public void startMoving(){
        if(getVisibility() != View.VISIBLE || isMoving){
            return;
        }
        isMoving = true;
        Moving = true;

        anim = ValueAnimator.ofFloat(0f,1f);
        anim.setDuration(2000);
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentValue = (float) animation.getAnimatedValue();

                dex = currentValue*mRectF.right*2f/3;
                centerX3-=dex;
                if(currentValue>0.9f ){
                    isClose = true;
                }else{
                    isClose = false;
                }

                postInvalidate();
            }


        });
        anim.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                isClose = false;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                isMoving = false;

                if(Moving){
                    float dest = centerX1;
                    centerX1 = centerX3;
                    centerX3 = centerX2;
                    centerX2 = dest;
                    startMoving();
                }

            }

            @Override
            public void onAnimationCancel(Animator animation) {
                centerX1 = mRectF.right+2f/3*mRectF.right-mRectF.right*1/5f;
                centerX2 = mRectF.right+4f/3*mRectF.right-mRectF.right*1/5f;
                centerX3 = mRectF.right+6/3*mRectF.right-mRectF.right*1/5f;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        anim.setInterpolator(new LinearInterpolator());
        anim.start();
    }

    public void stopMoving(){
        Moving = false;
        anim.cancel();
    }


}

参考文章:

Android开源:一款你不可错过的可爱&小资风格的加载等待控件库

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值