Android自定义Loading View

685 阅读4分钟

动画分析

先看一下实现的效果图:

loading.gif

看到这个图的时候是有点懵的,不要急,一帧一帧看动画的过程,然后拆解过程。

首先,可以把整体看成是个正方形, 上下两个圆角矩形, 如图所示:

image.png

因此整个动画可以均分为6步:

  • 1:方块1上升
  • 2:方块2上升
  • 3:方块3上升
  • 4:方块1下落
  • 5:方块2下落
  • 6:方块3下落

线条转动由于是从下方开始的,整个动画时间分为六段之后其实不用考虑线条和方块会不会撞到一起,因为它们有时间差,所以可以把这个view拆分成两部分处理,方块和线条。一步一步来实现,我的实现逻辑是:

  • 第一步,先绘制一个圆角矩形,调整宽高大小
  • 第二步,再只绘制圆角矩形中的1/6长度的线段,正好有个api可以取path的一部分:
//圆角矩形path, 顺时针方向
mLinePath.addRoundRect(mLineRect, mRoundRectWidth,mRoundRectWidth, Path.Direction.CW);
mPathMeasure = new PathMeasure(mLinePath,false);
Path dst = new Path();
//取mLinePath的一部分path, 结果存到dst
mPathMeasure.getSegment(start, end, dst, true);

getSegment这个方法要注意, 它限制了start 不能超过end, 所以下面的第三种情况只能分成两段线段绘制:

image.png

  • 第三步,让线段动起来。线段部分完成了,接下来是方块。
  • 第四步,绘制三个在底部的方块。
  • 第五步,绘制三个在顶部位置的方块。
  • 第六步,用动画实现三个方块的依次运动。
  • 第七步,线条动画联动方块动画。 完成!

直接放源码了,一个人的思维容易受限,有可以优化的地方还请大家指导指导。

实现源码

package com.skylar.demo;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
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.PathMeasure;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.BounceInterpolator;
import android.view.animation.LinearInterpolator;

import androidx.annotation.Nullable;

import java.math.BigDecimal;

//
// Created by skylar on 2022/4/9.
//
public class SearchLoadingView extends View {

    //线条和三个方块
    private Paint mLinePaint;
    private Paint mBlockPaint;

    private RectF mLineRect;
    private RectF mBlockRectOne;
    private RectF mBlockRectTwo;
    private RectF mBlockRectThree;

    private Path mLinePath;
    private Path mBlockPathOne;
    private Path mBlockPathTwo;
    private Path mBlockPathThree;
    private PathMeasure mPathMeasure;

    private int mViewWidth;
    private int mViewHeight;

    //线条相关
    private float mLineStartDistance; //线条开始位置
    private float mLineLength; //线条长度
    private float mRectLength; //线条圆角方框的总长度
    private float mLineWidth = 6; //线条宽度
    private int mRoundRectWidth = 10; //圆角矩形的圆角半径

    //方块相关
    private float mBlockWidth; //正方形方块的边长
    private float mBlockHorizontalSpace; //方块水平距离圆角方框的间距 (不包括圆角框的宽度mLineWidth)
    private float mBlockVerticalSpace; //方块垂直距离圆角方框的间距 (不包括圆角框的宽度mLineWidth)
    private float mBlockTopInBottom;  //方块在最底下位置时的top值 (RectF中的top)
    private float mBlockTopInUp; //方块在最上面位置时的top值 (RectF中的top)
    private float mBlockAnimHeight; //动画过程中运动方块的top值

    /* 当前动画处于第几阶段,共6个阶段
     *  1:方块1上升
     *  2:方块2上升
     *  3:方块3上升
     *  4:方块1下落
     *  5:方块2下落
     *  6:方块3下落
     */
    private int mStep = 1;

    //动画总时长
    private int mDuration = 1584;
    private ValueAnimator mLineAnimator;

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

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

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

    private void init() {
        setBackgroundColor(Color.BLACK);

        mLinePath = new Path();
        mLinePaint = new Paint();
        mLinePaint.setColor(Color.WHITE);
        mLinePaint.setStyle(Paint.Style.STROKE);
        mLinePaint.setStrokeWidth(mLineWidth);
        mLinePaint.setAntiAlias(true);

        mBlockPaint = new Paint();
        mBlockPaint.setColor(Color.WHITE);
        mBlockPaint.setAntiAlias(true);
        mBlockPaint.setStyle(Paint.Style.FILL);

        mBlockRectOne = new RectF();
        mBlockRectTwo = new RectF();
        mBlockRectThree = new RectF();
        mBlockPathOne = new Path();
        mBlockPathTwo = new Path();
        mBlockPathThree = new Path();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        mViewHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

        mLineWidth = divide(mViewWidth, 18);
        mLinePath.reset();
        mLinePaint.setColor(Color.WHITE);
        mLineRect = new RectF( mLineWidth / 2, 0, mViewWidth - mLineWidth / 2,mViewHeight / 2 - mLineWidth / 2);
        mLinePath.addRoundRect(mLineRect, mRoundRectWidth,mRoundRectWidth, Path.Direction.CW);
        mPathMeasure = new PathMeasure(mLinePath,false);
        mRectLength = mPathMeasure.getLength(); //得到圆角矩形的长度
        mLineLength = mRectLength / 6; //线条的长度

        //计算方块的大小,及水平和竖直间距
        mBlockHorizontalSpace = divide(mViewWidth - mLineWidth * 2, 15);
        mBlockWidth = mBlockHorizontalSpace * 3 ;
        mBlockVerticalSpace = divide(mLineRect.height() - mBlockWidth - mLineWidth * 1.5f, 2);

        mBlockTopInBottom = mLineWidth + mBlockVerticalSpace;
        mBlockTopInUp =  - (mLineRect.height() - mBlockVerticalSpace);

        float blockOneLeft = mLineWidth + mBlockHorizontalSpace * 2;
        mBlockRectOne.set(blockOneLeft,mBlockTopInBottom,blockOneLeft + mBlockWidth, mBlockTopInBottom + mBlockWidth);
        mBlockRectTwo.set(mBlockRectOne.left + mBlockWidth + mBlockHorizontalSpace, mBlockTopInBottom, mBlockRectOne.left + mBlockWidth + mBlockHorizontalSpace + mBlockWidth, mBlockTopInBottom + mBlockWidth);
        mBlockRectThree.set(mBlockRectTwo.left + mBlockWidth + mBlockHorizontalSpace, mBlockTopInBottom, mBlockRectTwo.left + mBlockWidth + mBlockHorizontalSpace + mBlockWidth, mBlockTopInBottom + mBlockWidth);
        mBlockPathOne.reset();
        mBlockPathTwo.reset();
        mBlockPathThree.reset();
        mBlockPathOne.addRect(mBlockRectOne, Path.Direction.CW);
        mBlockPathTwo.addRect(mBlockRectTwo, Path.Direction.CW);
        mBlockPathThree.addRect(mBlockRectThree, Path.Direction.CW);

        startAnimator();
    }

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

        canvas.translate(getPaddingLeft(), getPaddingTop() + mViewHeight / 2);

        //画线
        Path dst = new Path();
        mPathMeasure.getSegment(mLineStartDistance, mLineStartDistance + mLineLength, dst, true);
        canvas.drawPath(dst, mLinePaint);

        //getSegment有限制, end到start要分两截线段绘制
        if(mLineStartDistance + mLineLength > mRectLength) {
            //第二段line
            float secondPathLength = mLineStartDistance + mLineLength - mRectLength;
            Path dstSecond = new Path();
            mPathMeasure.getSegment(0, secondPathLength, dstSecond, true);
            canvas.drawPath(dstSecond, mLinePaint);
        }

        //画方块
        drawBlock(canvas);
    }

    private void drawBlock(Canvas canvas) {
        float blockOneLeft = mLineWidth + mBlockHorizontalSpace * 2;
        if(mStep == 1 || mStep == 4) { //第一个方块运动
            mBlockRectOne.set(blockOneLeft,mBlockAnimHeight,blockOneLeft + mBlockWidth, mBlockAnimHeight+mBlockWidth);
            mBlockPathOne.reset();
            mBlockPathOne.addRect(mBlockRectOne, Path.Direction.CW);
        } else if(mStep == 2 || mStep == 5) { //第二个方块运动
            mBlockRectTwo.set(blockOneLeft + mBlockWidth + mBlockHorizontalSpace, mBlockAnimHeight, blockOneLeft + mBlockWidth + mBlockHorizontalSpace + mBlockWidth, mBlockAnimHeight + mBlockWidth);
            mBlockPathTwo.reset();
            mBlockPathTwo.addRect(mBlockRectTwo, Path.Direction.CW);
        } else { //第三个方块运动
            mBlockRectThree.set(mBlockRectTwo.left + mBlockWidth + mBlockHorizontalSpace, mBlockAnimHeight, mBlockRectTwo.left + mBlockWidth + mBlockHorizontalSpace + mBlockWidth, mBlockAnimHeight + mBlockWidth);
            mBlockPathThree.reset();
            mBlockPathThree.addRect(mBlockRectThree, Path.Direction.CW);
        }

        canvas.drawPath(mBlockPathOne, mBlockPaint);
        canvas.drawPath(mBlockPathTwo, mBlockPaint);
        canvas.drawPath(mBlockPathThree, mBlockPaint);
    }

    /* 动画分为三个动画
     * 1. 线段转圈动画,一圈为一次动画,一直循环
     * 2. 方块的上升和下落动画,一次上升+下落的时间等于一次转圈的时间
     */
    private void startAnimator() {
        stopAnimator();
        mLineAnimator = ValueAnimator.ofFloat(0, mRectLength);
        mLineAnimator.setDuration(mDuration);
        mLineAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mLineAnimator.setInterpolator(new LinearInterpolator());
        float startDistance = mRectLength - mLineLength - mRoundRectWidth * 2;
        mLineAnimator.addUpdateListener(animation -> {
            mLineStartDistance = (startDistance + (float) animation.getAnimatedValue()) % mRectLength;
            invalidate();
        });
        mLineAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationRepeat(Animator animation) {
                super.onAnimationRepeat(animation);
                startBlockUpAndDown();
            }
        });
        mLineAnimator.start();
        startBlockUpAndDown();
    }

    private void startBlockUpAndDown() {
        ValueAnimator upAnimator = ValueAnimator.ofFloat(mBlockTopInBottom, mBlockTopInUp);
        upAnimator.setDuration(mDuration / 6);
        upAnimator.setRepeatCount(2);
        upAnimator.setInterpolator(new LinearInterpolator());
        upAnimator.addUpdateListener(animation -> mBlockAnimHeight = (float) animation.getAnimatedValue());
        upAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                mStep = 1;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                super.onAnimationRepeat(animation);
                mStep++;
            }
        });
        upAnimator.start();

        ValueAnimator downAnimator = ValueAnimator.ofFloat(mBlockTopInUp, mBlockTopInBottom);
        downAnimator.setDuration(mDuration / 6);
        downAnimator.setRepeatCount(2);
        downAnimator.setInterpolator(new BounceInterpolator());
        downAnimator.addUpdateListener(animation -> mBlockAnimHeight = (float) animation.getAnimatedValue());
        downAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                mStep++;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                super.onAnimationRepeat(animation);
                mStep++;
            }
        });

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.play(upAnimator).before(downAnimator);
        animatorSet.start();
    }


    public void stopAnimator() {
        if(mLineAnimator != null) {
            mLineAnimator.cancel();
        }
    }

    //arg1 除以 arg2 的精确结果, 直接用 / 会丢失准度
    private float divide(float arg1, float arg2) {
        BigDecimal b1 = new BigDecimal(Float.toString(arg1));
        BigDecimal b2 = new BigDecimal(Float.toString(arg2));
        return  b1.divide(b2, 2, BigDecimal.ROUND_HALF_UP).floatValue();
    }
}