简单的加载动画

今天手机被测试同学征用了……想想好久没有写文章了,正好来水一篇。(近期也在学习音视频的东西,因为还没了解透彻不敢乱写,这几天应该也会慢慢补上)。

UI同学给出的效果图是这样的


LoadingUI.gif
1. 拆分动画

从动画里可以看出,整个动画分4个阶段
(1)主圆点从左向右
(2)主圆点在到达绿色点时减速到0,并且左边界重合
(3)主圆点开始向左运动
(4)主圆点到达红色点时减速到0,重新开始1
其中,① 点与点之间的动画可以拆分开来。② 点与点之 间主圆点会渐变颜色

2. 分析动画

很明显,主圆点的吸附效果是一个贝塞尔曲线的动画,是不是感觉似曾相识?我起初也这么觉得,于是开始找一些效果,找到了SpringIndicatorBezierIndicator两种效果。

SpringIndicator

BezierIndicator

其实仔细想想,QQ的气泡拖动已读也是SpringIndicator的效果,对比之下显然是SpringIndicator中的效果更符合UI的预期,四个点的动画也都可以拆分成两两之间的动画。

最难的部分已经找到解决方案了,剩下在边界顶点的移动也只是简单的动画了,接下来可以开始编码了。

3. 编写动画
  1. 首先要把四个小圆点画出来
  2. 画出主圆点
  3. 设定牵引点(就是目标点)的坐标和尾巴点(原始位置)坐标
  4. 设定动画参数和边界条件(用来控制哪里需要切断曲线)

自定义BubbleView,用来画5个圆点。(为了简洁,以下代码段都省去了部分代码)

public class BubbleView extends View {

    private Paint mPaint;
    // 气泡颜色
    private int mBubbleColor = Color.GRAY;
    // 气泡半径
    private int mRadius = 100;
    private float mBubbleX;
    private float mBubbleY;

    private int mBubbleHeight;
    private int mBubbleWidth;
    private PointF pointF;

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

    public BubbleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setColor(mBubbleColor);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setStrokeWidth(1);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mBubbleHeight = getMeasuredHeight();
        mBubbleWidth = getMeasuredWidth();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(mBubbleWidth >> 1, mBubbleHeight >> 1);
        canvas.drawCircle(0, 0, mRadius, mPaint);
    }
}

自定义ViewGroup摆放5个圆点的位置

public class BubbleContainer extends LinearLayout {

    private static final int ANIMATION_DURATION = 16000;
    private static final int REPEAT_COUNT = Animation.INFINITE;

    private int[] mColors = {Color.parseColor("#ff3925"),
            Color.parseColor("#ff8c14"),
            Color.parseColor("#0091d7"),
            Color.parseColor("#50c414"),
    };

    private int mRadius = dp2px(3);
    private int mBigBubbleRadius = dp2px(5);
    private int mMaxInterval = dp2px(8);

    private ArrayList<BubbleView> mBubbles;
    private SpringView springView;
    private final int defaultInterval = -mRadius;
    private int mBubbleInterval = defaultInterval;
    private boolean mStopFlag;
    private int mWidth;
    private BubbleState mBubbleState;
    private ValueAnimator valueAnimator;

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

    public BubbleContainer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BubbleContainer(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setOrientation(HORIZONTAL);
        setWillNotDraw(false);

        mBubbles = new ArrayList<>(4);
        for (int i = 0; i < 4; i++) {
            BubbleView bubble = new BubbleView(getContext());
            bubble.setColor(mColors[i]);
            bubble.setRadius(mRadius);

            LinearLayout.LayoutParams params = new LayoutParams(2 * mRadius, 2 * mRadius);
            mBubbles.add(bubble);
            // 将圆点添加到ViewGroup
            addView(bubble, params);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int middleWidth = (r - l) / 2;
        int middleHeight = (b - t) / 2;

        if (springView == null) {
            addPointView(r - l, b - t);
        }

        int childCount = mBubbles.size();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            BubbleView bubble = mBubbles.get(i);
            layoutChildBubble(bubble, i, middleWidth, middleHeight);

            bubble.setBubbleX(child.getX() + mRadius);
            bubble.setBubbleY(child.getY() + mRadius);
        }

        springView.layout(l, middleHeight - mBigBubbleRadius, r, middleHeight + mBigBubbleRadius);

        if (mBubbleState == null) {
            mBubbleState = new BubbleState(springView);
            mBubbleState.setBubbles(mBubbles);
        }
    }

    // 摆放4个圆点
    private void layoutChildBubble(BubbleView bubble, int position, int middleWidth, int middleHeight) {
        int distance = mBubbleInterval + mRadius;
        switch (position) {
            case 0:
                bubble.layout(middleWidth - 3 * distance - mRadius,
                        middleHeight - mRadius,
                        middleWidth - 3 * distance + mRadius,
                        middleHeight + mRadius);
                break;
            case 1:
                bubble.layout(middleWidth - distance - mRadius,
                        middleHeight - mRadius,
                        middleWidth - distance + mRadius,
                        middleHeight + mRadius);
                break;
            case 2:
                bubble.layout(middleWidth + mBubbleInterval,
                        middleHeight - mRadius,
                        middleWidth + distance + mRadius,
                        middleHeight + mRadius);
                break;
            case 3:
                bubble.layout(middleWidth + 3 * mBubbleInterval + 2 * mRadius,
                        middleHeight - mRadius,
                        middleWidth + 3 * mBubbleInterval + 4 * mRadius,
                        middleHeight + mRadius);
                break;
        }
    }
}

然后加入主圆点

 private void addPointView(int width, int height) {
        springView = new SpringView(getContext());
        springView.setIndicatorColor(mColors[0]);
        LinearLayout.LayoutParams params = new LayoutParams(width, height);
        addView(springView, params);
    }

初始效果

为主圆点加入动画。主圆点的动画模仿了SpringIndicator的模式,不过这里加入了3个点,一个牵引一个尾巴,还有主圆点,这样更能表现水滴粘滞的效果。这里具体的绘画流程不做具体分析,感兴趣的同学可以直接参看Android 贝塞尔曲线解析

   // 贝塞尔曲线绘制数据
   private void makePath() {
        float headOffsetX = (float) (headPoint.getRadius() * Math.sin(Math.atan((footPoint.getY() - headPoint.getY()) / (footPoint.getX() - headPoint.getX()))));
        float headOffsetY = (float) (headPoint.getRadius() * Math.cos(Math.atan((footPoint.getY() - headPoint.getY()) / (footPoint.getX() - headPoint.getX()))));

        float footOffsetX = (float) (footPoint.getRadius() * Math.sin(Math.atan((footPoint.getY() - headPoint.getY()) / (footPoint.getX() - headPoint.getX()))));
        float footOffsetY = (float) (footPoint.getRadius() * Math.cos(Math.atan((footPoint.getY() - headPoint.getY()) / (footPoint.getX() - headPoint.getX()))));

        float pullOffsetX = (float) (pullPoint.getRadius() * Math.sin(Math.atan((headPoint.getY() - pullPoint.getY()) / (headPoint.getX() - pullPoint.getX()))));
        float pullOffsetY = (float) (pullPoint.getRadius() * Math.cos(Math.atan((headPoint.getY() - pullPoint.getY()) / (headPoint.getX() - pullPoint.getX()))));

        float x1 = headPoint.getX() - headOffsetX;
        float y1 = headPoint.getY() + headOffsetY;

        float x2 = headPoint.getX() + headOffsetX;
        float y2 = headPoint.getY() - headOffsetY;

        float x3 = footPoint.getX() - footOffsetX;
        float y3 = footPoint.getY() + footOffsetY;

        float x4 = footPoint.getX() + footOffsetX;
        float y4 = footPoint.getY() - footOffsetY;

        float x5 = headPoint.getX() + headOffsetX;
        float y5 = headPoint.getY() + headOffsetY;

        float x6 = headPoint.getX() - headOffsetX;
        float y6 = headPoint.getY() - headOffsetY;

        float x7 = pullPoint.getX() - pullOffsetX;
        float y7 = pullPoint.getY() + pullOffsetY;

        float x8 = pullPoint.getX() + pullOffsetX;
        float y8 = pullPoint.getY() - pullOffsetY;

        float anchorX = (footPoint.getX() + headPoint.getX()) / 2;
        float anchorY = (footPoint.getY() + headPoint.getY()) / 2;

        float anchorX2 = (headPoint.getX() + pullPoint.getX()) / 2;
        float anchorY2 = (headPoint.getY() + pullPoint.getY()) / 2;

        path.reset();

        if (Math.abs(pullPoint.getX() - headPoint.getX()) > headPoint.getRadius()) {
            path.moveTo(x1, y1);
            path.quadTo(anchorX, anchorY, x3, y3);
            path.lineTo(x4, y4);
            path.quadTo(anchorX, anchorY, x2, y2);
            path.lineTo(x1, y1);
        }

        if (Math.abs(pullPoint.getX() - headPoint.getX()) < headPoint.getRadius() + headPoint.getRadius()) {
            path.moveTo(x5, y5);
            path.quadTo(anchorX2, anchorY2, x7, y7);
            path.lineTo(x8, y8);
            path.quadTo(anchorX2, anchorY2, x6, y6);
            path.lineTo(x5, y5);
        }
    }

加入ValueAnimator控制主圆点位置

   private void headPointRadiusAnimation() {
        if (valueAnimator != null) {
            return;
        }
        float starX = mBubbles.get(0).getBubbleX();
        float endX = mBubbles.get(3).getBubbleX();

        valueAnimator = ValueAnimator.ofFloat(starX, endX, endX + mRadius, starX, starX - mRadius, starX);
        valueAnimator.setDuration(ANIMATION_DURATION);
        valueAnimator.setRepeatCount(REPEAT_COUNT);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animatedValue = (float) animation.getAnimatedValue();
                Point headPoint = springView.getHeadPoint();
                headPoint.setX(animatedValue);
                springView.invalidate();
            }
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                mStopFlag = true;
            }
        });
    }

在这之后,我们的圆点就可以动起来了,但是有一个问题,就是牵引点和尾巴点的位置没有改变,我们需要控制他们的位置和状态。

封装各个圆点的位置状态

class BubbleState {

    private ArrayList<BubbleView> bubbleViews;
    private Point footPoint;
    private Point pullPoint;
    private Point headPoint;

    private float x0;
    private float x1;
    private float x2;
    private float x3;

    private boolean back;
    private ValueAnimator colorValueAnimator;

    BubbleState(SpringView springView) {
        footPoint = springView.getFootPoint();
        headPoint = springView.getHeadPoint();
        pullPoint = springView.getPullPoint();
    }

    void setBubbles(ArrayList<BubbleView> bubbles) {
        bubbleViews = bubbles;

        x0 = bubbles.get(0).getBubbleX();
        x1 = bubbles.get(1).getBubbleX();
        x2 = bubbles.get(2).getBubbleX();
        x3 = bubbles.get(3).getBubbleX();
    }

    void setValue(float animatedValue) {
        if (back) {
            back(animatedValue);
        } else {
            forward(animatedValue);
        }
    }

    private void forward(float animatedValue) {
        if (x0 < animatedValue && animatedValue < x1) {
            state1();
        } else if (x1 <= animatedValue && animatedValue < x2) {
            state2();
        } else if (x2 <= animatedValue && animatedValue < x3) {
            state3();
        } else if (animatedValue >= x3) {
            state4();
            back = true;
        }
    }

    private void back(float animatedValue) {
        if (x0 < animatedValue && animatedValue < x1) {
            state7();
        } else if (x1 <= animatedValue && animatedValue < x2) {
            state6();
        } else if (x2 <= animatedValue && animatedValue < x3) {
            state5();
        } else if (animatedValue <= x0) {
            state8();
            back = false;
        }
    }

    void initState() {
        state1();
    }

    /**
     * 0-1
     */
    private void state1() {
        BubbleView foot = bubbleViews.get(0);
        BubbleView pull = bubbleViews.get(1);

        setState(pull, foot);
    }

    /**
     * 1-2
     */
    private void state2() {
        BubbleView foot = bubbleViews.get(1);
        BubbleView pull = bubbleViews.get(2);

        setState(pull, foot);
    }
    // ...省略部分状态设置

    private void setState(BubbleView pull, BubbleView foot) {
        setX(pull, foot);
        setColor(pull, foot);
    }

    private void setX(BubbleView pull, BubbleView foot) {
        if (pull.getBubbleX() == pullPoint.getX()) {
            return;
        }
        pullPoint.setX(pull.getBubbleX());
        footPoint.setX(foot.getBubbleX());
    }

    private void setColor(BubbleView pull, BubbleView foot) {
        setHeadColor(pull, foot);

        if (pull.getBubbleColor() == pullPoint.getColor()) {
            return;
        }
        pullPoint.setColor(pull.getBubbleColor());
        footPoint.setColor(foot.getBubbleColor());
    }

    private void setHeadColor(BubbleView pull, BubbleView foot) {
        if (Math.abs(headPoint.getX() - foot.getBubbleX()) > headPoint.getRadius()) {
            headPoint.setColor(pull.getBubbleColor());
        } else {
            headPoint.setColor(foot.getBubbleColor());
        }
    }
}

这样在ValueAnimator中只需要不断调用X的值就能安排好每个圆点的位置了

valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animatedValue = (float) animation.getAnimatedValue();
                mBubbleState.setValue(animatedValue);
                springView.invalidate();
            }
        });

Loading.gif

这里的圆点间距、大小、颜色、主圆点的速度都可以调整。唯一不足的就是颜色的渐变效果不明显,我尝试使用系统和自定义的颜色渐变都不能很好的解决,因为我们自定的是按一定的规则变更颜色数值的,但是这里的颜色排列并不是按照颜色渐变来的,在第二个点到第三个点的过程中,圆点会先变成绿色再变成蓝色,这样显然是有问题的,解决的方案也是在BubbleState中加入颜色的渐变,不过因为速度太快,这样调整的投入产出比太低,UI同学也认可了上面的动画,就没有进一步的修改,不过我认为还是有时间补上吧,虽然最近并没有
(:з」∠)

写的确实有点水了……具体还是看代码吧BubbleLoading

参考 SpringIndicator

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