记一次自定义Loading的心里路程

事情是这样的0-0,这个是我的一个好基友Anonymous___童鞋发给我的一个效果图,主要是练手一下最近研究的自定义view的各种技能,于是就搞了一下,最终实现其实很简单,但是中间也绕了几次弯,先上图,这是原图效果:

preloader.gif

下面是代码实现效果:
loading.gif

可能在细节方面有些差异,但是重点是实现方式,是吧- -。
首先分析一下动画的变化过程,姑且把它看成是一段圆弧的变化吧,一共可以分为一下几点:

  1. 颜色变化,颜色会从青色变为蓝色;
  2. 圆弧线条粗细的变化,刚开始是整个填充的,然后旋转一小节之后,线条开始变细;
  3. 圆弧旋转一周之后,长度开始缩小,并在最后延长一部分

好了,动画分析的差不多了,那么就开始实现吧。

第一步,画圆弧

图中的圆弧大概长这个样子吧

圆弧.png

这个简单,直接使用一个Path类的addArc(),然后用Canvas画出来就可以了,可是怎么画出圆弧的轮廓呢,这个当然你可以画两个圆弧加两个半圆去实现,但是太麻烦了,其实在Paint中有一个方法getFillPath (Path src, Path dst)专门可以获取到Canvas画出的path的实际的path,但是使用这个方法,记得关闭硬件加速!!!,关闭硬件加速请参考这里Android如何关闭硬件加速,关于Paint的Api详解可以到扔物线大大的HenCoder中详细了解,我就不做过多的赘述了。效果大概就是下面这个样子,第一个图是原Path画出的圆弧,第二个是获取到的轮廓:

arc_src.png

arc_dst.png

第二步,执行动画

这里我大概想到了两种方式:

第一种方式

直接使用属性动画(ObjectAnimator),去动态的改变自定义View的属性值,比如我们可以在自定义View中定义一个变量progessColor来记录颜色,并给这个变量定义setter方法,在setter方法中调用invalidate方法,这样就可以通过ObjectAnimator来动态的改变progessColor的值,来一直刷新绘制方法onDraw,从而让view“动”起来;同样的方式可以改变绘制圆弧的起始角度startAngle和绘制View的线条粗细progessWidth,然后同时执行这三个动画,就可以让圆弧转起来了,不过这只是前半段动画,还有后半段动画,让圆弧的长度缩小并在最后突出一小截,这个问题可以给ObjectAnimator设置监听,在动画结束后,通过一个handler通知下一个动画执行,缩小圆弧长度可以控制绘制圆弧角度的sweepAngle属性,让其不断的减小即可,对属性动画不熟悉的同学,同样可以去看抛物线大大的HenCoder,大致代码及效果如下:

 public void setProgessColor(int progessColor) {
        this.progessColor = progessColor;
        invalidate();
}
... 省略部分代码 ...
ObjectAnimator objectAnimator = ObjectAnimator.ofInt(this, "progessColor", startColor, endColor);
objectAnimator.setEvaluator(new ArgbEvaluator());          // 使用ARGB求值器来改变颜色
objectAnimator.setDuration(2000);

ObjectAnimator objectAnimator1 = ObjectAnimator.ofFloat(this, "progessWidth", startWidth, endWidth);
objectAnimator1.setDuration(2000);

ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(this, "startAngle", startAngle, startAngle + 360);
objectAnimator2.setDuration(2000);

objectAnimator3 = objectAnimator.ofFloat(this, "sweepAngle", sweepAngle - 2, 10, 0, -10); // 需要在最后突出一截
objectAnimator3.setDuration(3000);

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(objectAnimator, objectAnimator1, objectAnimator2);      // 使用AnimatorSet同时执行动画
animatorSet.start();

loading1.gif

这种方式大家应该也看出来了,有几个缺陷,第一,原动画的弧线是运动一段时间,线的宽度才开始减小的,而这个动画直接就开始减小;第二,中间动画衔接的时候会有一小点停顿的状态;第三,这个动画得倒回去啊。当然使用setRepeatMode(ValueAnimator.REVERSE) 和 setRepeatCount(ValueAnimator.INFINITE)Api可以让动画倒回去,但是还需要控制动画执行顺序,太麻烦,遂弃之。

第二种方式

既然使用ObjectAnimator不好使,有限制,那么我们就换一个他的爹地,更加灵活的ValueAnimator,它的玩法非常简单,只需要调用ofFloat(),然后再添加addUpdateListener()监听,在update的过程中,不断的根据自己设计的算法改变对应的值就ok了,当然关于颜色的变化还是需要使用ObjectAnimator的方法,因为颜色的变化需要ArgbEvaluator求值器来计算,具体的算法在下面的代码里做解释:

ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
// 设置update监听,并在属性更新时写自己的算法
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        // 获取到当前动画的完成度,取值为0~1
        float animatedFraction = animation.getAnimatedFraction();         
        // 用来计算线条宽度的变化,前动画的前0.2不改变初始线条宽度,从0.2~0.9,动态的将线条宽度变为最终的宽的endWidth
        // 0.9~1阶段,宽度始终为最终宽度endWidth
        progessWidth = startWidth - ((animatedFraction - 0.2f) / 0.7f) * startWidth;       
        // 前0.2的变化会进入这个判断
        if (progessWidth >= startWidth) {    
            progessWidth = startWidth;
        }
        // 0.9~1时会进这个判断
        if (progessWidth <= endWidth) {      
            progessWidth = endWidth;
        }
        // 用来计算弧线长度的变化,前0.7的变化为startAngle从50度逐渐变化为450,sweepAngle固定不变,即为固定的弧长旋转动画
        if (animatedFraction <= 0.7f) {
            startAngle = 50 + animatedFraction / 0.7f * 400f;
            sweepAngle = -100;
        }
        // 从0.7~1的动画为startAngle不变,sweepAngle逐渐减小再增大的过程(这个是绘制长度的减小,符号代表绘制圆弧的方向)
        // 即从-100变到20,从而达到长度开始缩小,并在最后延长一部分的动画
        if (animatedFraction > 0.7f) {
            startAngle = 450;
            sweepAngle = -100 + ((animatedFraction - 0.7f) / 0.25f) * 100;
        }
        invalidate();    // 别忘了invalidate触发重绘onDraw
    }
});
// 动态改变颜色
ObjectAnimator objectAnimator = ObjectAnimator.ofInt(this, "progessColor", startColor, endColor);
objectAnimator.setEvaluator(new ArgbEvaluator());
// 使用AnimatorSet同时执行两种动画
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(valueAnimator, objectAnimator);
animatorSet.start();

上面代码中,0.7f了、0.2f了这些值都是可以自己改的,不同的值效果也会不一样,大家可以自己修改试一下看看效果。先看看这个实现的效果图:


loading2.gif

是不是觉得已经ok了呢,nonono,还有最后一步,这个loading动画是循环播放的呢,而且需要倒回去,所以:

第三步,REVERSE

这个就很简单了,给动画设置setRepeatModesetRepeatCount就行了。

valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);

objectAnimator.setRepeatMode(ValueAnimator.REVERSE);
objectAnimator.setRepeatCount(ValueAnimator.INFINITE);

效果就不贴了,就是本文开头的那个效果。代码虽然很简单,但是重要的还是思路,这东西就跟魔术一样,看到的时候感觉很神奇,但是知道了原理就发现原来就那么回事。

总结一下

这种动画主要是通过使用valueAnimatorAnimatorUpdateListener中动态的改变需要绘制的图形的属性值,并不断的通过invalidate触发onDraw,从而使得图形在感官上是“动”起来。这样你就可以根据你自己的算法做出各种动画了。我的好基友的系列文章也挺不错的,推荐大家一看。当然想要做出这些东西是需要一定的自定义View的基础的,首先你得知道各种Api的使用和细节吧,巧妇还难为无米之炊呢,下面我推荐几个网站:

  1. 我大Google的官方文档,Canvas,Path,Paint,PathMeasure,Camera等等一些类的Api总得玩儿转吧,什么?你说英语差?机翻会不。不过还是希望大家没事儿了多背背单词,不能一直靠机翻吧,多low(开玩笑~~~)
  2. 再次推荐抛物线大大的HenCoder教程,内容不多,但是是精品
  3. GcsSloop的魔法首页,写的非常细,配合HenCoder食用更加

就酱,代码很简单,贴下面咯,如果大家觉得ok,那么记得点赞哈,还有,强烈推荐看看我推荐的那几个网站噻。
使用下面的代码请记得关闭硬件加速!!!关闭硬件加速!!!关闭硬件加速!!!重要的事说三遍,在清单文件的application中加android:hardwareAccelerated="false"即可

package com.moonight.customview;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

/**
 * Created by MooNight on 2017/9/11.
 */

public class CustomProgressView extends View {

    private Paint mPaint;

    private float left = -100;
    private float top = -100;
    private float right = 100;
    private float bottom = 100;

    private float startAngle = 90;
    private float sweepAngle = -100;

    private int mViewWidthHalf, mViewHeightHalf;        // 获取view的宽高的一半

    private Path arcSrcPath;
    private Path arcDstPath;

    private int startColor = 0xFF10D2DE;
    private int endColor = 0xFF1039DD;
    private int progessColor = startColor;

    private float progessWidth;
    private float startWidth = 70;
    private float endWidth = 5;
    private RectF rectF;


    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            objectAnimator3.start();                //当第阶段动画执行完毕后,执行第二阶段动画
        }
    };
    private ObjectAnimator objectAnimator3;
    private Animator.AnimatorListener animatorListener;

    public CustomProgressView(Context context) {
        this(context, null);
    }

    public CustomProgressView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }


    // 提供setter方法,使用ObjectAnimator改变对应的属性值
    public void setProgessColor(int progessColor) {
        this.progessColor = progessColor;
        invalidate();
    }
    public void setProgessWidth(float progessWidth) {
        this.progessWidth = progessWidth;
    }

    public void setStartAngle(float startAngle) {
        this.startAngle = startAngle;
    }

    public void setSweepAngle(float sweepAngle) {
        this.sweepAngle = sweepAngle;
        invalidate();                       // 当属性值改变时,调用invalidate出发onDraw回调
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidthHalf = w / 2;             // 在onSizeChanged中获取view的宽高值
        mViewHeightHalf = h / 2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(Color.BLACK);              // 绘制背景
        canvas.save();
        canvas.translate(mViewWidthHalf, mViewHeightHalf);      // 将绘制中心坐标(0,0)移动到View中心

        arcSrcPath = new Path();                    // 每次都绘制新的path
        arcDstPath = new Path();

        arcSrcPath.addArc(rectF, startAngle, sweepAngle);
        mPaint.setStrokeWidth(startWidth);
        mPaint.getFillPath(arcSrcPath, arcDstPath);     // 获取实际弧线path的轮廓
        canvas.clipPath(arcDstPath);                    // 只显示轮廓部分,如果不这么做,会使得出事弧线过宽

        mPaint.setColor(progessColor);
        mPaint.setStrokeWidth(progessWidth);            // 动态改变paint的颜色跟线宽
        canvas.drawPath(arcDstPath, mPaint);

        canvas.restore();
    }

    private void init() {
        initPaint();
        initRect();
        initAnimator();
    }

    private void initAnimator() {
//        initAnimator1();

        initAnimator2();
    }

    /**
     * 第二种实现方式
     */
    private void initAnimator2() {
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        valueAnimator.setDuration(2200);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 获取到当前动画的完成度,取值为0~1
                float animatedFraction = animation.getAnimatedFraction();
                // 用来计算线条宽度的变化,前动画的前0.2不改变初始线条宽度,从0.2~0.9,动态的将线条宽度变为最终的宽的endWidth
                // 0.9~1阶段,宽度始终为最终宽度endWidth
                progessWidth = startWidth - ((animatedFraction - 0.2f) / 0.7f) * startWidth;
                // 前0.2的变化会进入这个判断
                if (progessWidth >= startWidth) {
                    progessWidth = startWidth;
                }
                // 0.9~1时会进这个判断
                if (progessWidth <= endWidth) {
                    progessWidth = endWidth;
                }
                // 用来计算弧线长度的变化,前0.7的变化为startAngle从50度逐渐变化为450,sweepAngle固定不变,即为固定的弧长旋转动画
                if (animatedFraction <= 0.7f) {
                    startAngle = 50 + animatedFraction / 0.7f * 400f;
                    sweepAngle = -100;
                }
                // 从0.7~1的动画为startAngle不变,sweepAngle逐渐减小再增大的过程(这个是绘制长度的减小,符号代表绘制圆弧的方向)
                // 即从-100变到20,从而达到长度开始缩小,并在最后延长一部分的动画
                if (animatedFraction >= 0.7f) {
                    startAngle = 450;
                    sweepAngle = -100 + ((animatedFraction - 0.7f) / 0.25f) * 100;
                }
                invalidate();     // 别忘了invalidate触发重绘onDraw
            }
        });

        // 动态改变颜色
        ObjectAnimator objectAnimator = ObjectAnimator.ofInt(this, "progessColor", startColor, endColor);
        objectAnimator.setRepeatMode(ValueAnimator.REVERSE);
        objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
        objectAnimator.setEvaluator(new ArgbEvaluator());
        objectAnimator.setDuration(2200);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(valueAnimator, objectAnimator);
        animatorSet.start();
    }

    /**
     * 第一种实现方式
     */
    private void initAnimator1() {
        initObjectAnimatorListener();
        initObjectAnimator();
    }

    private void initObjectAnimatorListener() {
        animatorListener = new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                handler.sendEmptyMessage(0);
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        };
    }

    private void initObjectAnimator() {
        ObjectAnimator objectAnimator = ObjectAnimator.ofInt(this, "progessColor", startColor, endColor);
//        objectAnimator.setRepeatMode(ValueAnimator.REVERSE);
//        objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
        objectAnimator.setEvaluator(new ArgbEvaluator());
        objectAnimator.addListener(animatorListener);
        objectAnimator.setDuration(2000);

        ObjectAnimator objectAnimator1 = ObjectAnimator.ofFloat(this, "progessWidth", startWidth, endWidth);
//        objectAnimator1.setRepeatMode(ValueAnimator.REVERSE);
//        objectAnimator1.setRepeatCount(ValueAnimator.INFINITE);
        objectAnimator1.setDuration(2000);

        ObjectAnimator objectAnimator2 = ObjectAnimator.ofFloat(this, "startAngle", startAngle, startAngle + 360);
        objectAnimator2.setInterpolator(new AccelerateDecelerateInterpolator());
//        objectAnimator2.setRepeatMode(ValueAnimator.REVERSE);
//        objectAnimator2.setRepeatCount(ValueAnimator.INFINITE);
        objectAnimator2.setDuration(2000);

        objectAnimator3 = objectAnimator.ofFloat(this, "sweepAngle", sweepAngle - 2, 10, 0, -10);
        objectAnimator3.setDuration(3000);

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(objectAnimator, objectAnimator1, objectAnimator2);
        animatorSet.start();
    }

    private void initRect() {
        rectF = new RectF(left, top, right, bottom);
    }

    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);      // 线端点处设置会圆头
        mPaint.setStrokeWidth(startWidth);
        mPaint.setColor(startColor);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,085评论 25 707
  • 为什么会突然学习硬件加速呢?因为在绘图的时候,并不是所有的函数都支持硬件加速,我就有一个疑问,硬件加速不是好东西来...
    黑白咖阅读 14,656评论 2 34
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,066评论 5 13
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,421评论 6 30
  • 要点 1.如何让自己改变:慢慢来,每天一点小进步,持续不断的学习,当你自制力不强时,加入一个学习小组监督自己学习。...
    那年我们正值青春阅读 139评论 0 0