仿狗东加载效果—支持加载成功和加载失败动画效果和颜色自定义的view

  最近在爬坑自定义View,看到狗东支付时有一个支付成功后的动画效果。

  遂决定自己也撸一个,加入了自己的一些想法,把实现的思路分享一下。
特点:

  1. 加载的view元素颜色支持自定义
  2. 加载成功和加载失败会有一个动画效果

源码已上传GitHub PowerfulLoadingView ,欢迎交流。

先上效果图

这里写图片描述

自定义属性

新建 res/values/attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PowerfulLoadingView">
        <!--加载视图背景的颜色-->
        <attr name="bg_color" format="color"/>
        <!--加载条的颜色-->
        <attr name="loading_bar_color" format="color"/>
        <!--钩和叉的颜色-->
        <attr name="tick_cross_color" format="color"/>
    </declare-styleable>
</resources>

老规矩,构造方法三连击。然后进行自定义属性值的读取。如果未指定自定义属性,则使用默认值。
这里new了两个画笔,一个用于画线和圆弧,一个用于画实心圆

    private static final int DEFAULT_CONTENT_COLOR = Color.WHITE;
    private static final int DEFAULT_LOADING_BAR_COLOR = Color.rgb(65, 105, 225);
    private static final int DEFAULT_BG_COLOR = Color.argb(55, 0, 0, 0);
    private int mLoadingBarColor = DEFAULT_LOADING_BAR_COLOR;
    private int mLoadingBgColor = DEFAULT_BG_COLOR;
    private int mTickOrCrossColor = DEFAULT_CONTENT_COLOR;

    public PowerfulLoadingView(Context context) {
        super(context);
        init(context, null);
    }

    public PowerfulLoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

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

    private void init(Context context, @Nullable AttributeSet attrs) {
        mContext = context;
        //用于画实心圆
        mPaintFill = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintFill.setStyle(Paint.Style.FILL);
        //用于画线和圆弧
        mPaintStroke = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintStroke.setStyle(Paint.Style.STROKE);
        mPaintStroke.setStrokeWidth(STROKE_WIDTH);

        if (attrs != null) {
            TypedArray array = mContext.obtainStyledAttributes(attrs, R.styleable.PowerfulLoadingView);
            for (int i = 0; i < array.getIndexCount(); i++) {
                int attr = array.getIndex(i);

                switch (attr) {
                    case R.styleable.PowerfulLoadingView_bg_color:
                        mLoadingBgColor = array.getColor(attr, DEFAULT_BG_COLOR);
                        break;

                    case R.styleable.PowerfulLoadingView_loading_bar_color:
                        mLoadingBarColor = array.getColor(attr, DEFAULT_LOADING_BAR_COLOR);
                        break;

                    case R.styleable.PowerfulLoadingView_tick_cross_color:
                        mTickOrCrossColor = array.getColor(attr, DEFAULT_CONTENT_COLOR);
                        break;
                }
            }
            array.recycle();
        }
    }

加载中效果实现

  看下加载效果,是一条圆弧在围绕中心转动。这里采用 ValueAnimator 来实现。
  定义一个0~360的范围,表示角度360度变化。在 setDuration 设置的时间内,AnimatedValue 会从0递增到360,值每变化一次,都会调用 onDraw 方法进行重绘。每次进入onDraw方法,都会通过 drawCircle 先绘制一个圆形的背景,然后通过 getAnimatedValue 方法获取当前的值(表示角度值),然后通过 drawArc 方法绘制圆弧。只需要改变绘制圆弧时的传入的角度参数,就可以实现旋转的效果。

    private ValueAnimator mCircleAngleAnimator;

    public void startLoading() {
        clearAllAnimator();

        //初始化加载条动画,并循环播放
        mCircleAngleAnimator = ValueAnimator.ofFloat(0, 360);
        mCircleAngleAnimator.setDuration(ANIMATOR_TIME);
        mCircleAngleAnimator.setRepeatMode(ValueAnimator.RESTART);
        mCircleAngleAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mCircleAngleAnimator.start();
    }

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

        //绘制转动的加载条
        if (mCircleAngleAnimator != null && mCircleAngleAnimator.isRunning()) {
            drawBackground(canvas, mLoadingBgColor);
            mPaintStroke.setColor(mLoadingBarColor);
            mRectF.set(STROKE_WIDTH * 2, STROKE_WIDTH * 2,
                    getWidth() - STROKE_WIDTH * 2, getHeight() - STROKE_WIDTH * 2);
            canvas.drawArc(mRectF, (float) mCircleAngleAnimator.getAnimatedValue(), 270, false, mPaintStroke);
            invalidate();
        }
        ......
    }

    //绘制背景
    private void drawBackground(Canvas canvas, int color) {
        mPaintFill.setColor(color);
        canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, getWidth() / 2f, mPaintFill);
    }

加载成功和加载失败效果实现

确定钩和叉的大小和位置

  绘制加载完成的动画之前,需要先确定钩和叉的大小和位置。这一步在 onMeasure 中完成。
  分析一下,可以知道,确定一个钩的大小和位置,需要确定三个点的坐标。确定一个叉的大小和位置,需要确定四个点的坐标。所以这里定义两个 Point 对象的数组来存储这些坐标信息。
  这里采用view整个画布的中心点,来做为参考点,来确定这个7个点的坐标。具体可以自行调整。

    private Point[] mTickPoint = new Point[3];
    private Point[] mCrossPoint = new Point[4];

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        measureTickPosition(width / 2, height / 2);
        measureCrossPosition(width / 2, height / 2);
    }

    //测量钩的大小和位置
    private void measureTickPosition(int centerX, int centerY) {
        Point position = new Point();
        position.x = centerX / 2;
        position.y = centerY;
        mTickPoint[0] = position;

        position = new Point();
        position.x = centerX / 10 * 9;
        position.y = centerY + centerY / 3;
        mTickPoint[1] = position;

        position = new Point();
        position.x = centerX + centerX / 2;
        position.y = centerY / 3 * 2;
        mTickPoint[2] = position;
    }

    //测量叉的大小和位置
    private void measureCrossPosition(int centerX, int centerY) {
        Point position = new Point();
        position.x = centerX / 3 * 2;
        position.y = centerY / 3 * 2;
        mCrossPoint[0] = position;

        position = new Point();
        position.x = centerX / 3 * 2;
        position.y = centerY + centerY / 3;
        mCrossPoint[1] = position;

        position = new Point();
        position.x = centerX + centerX / 3;
        position.y = centerY + centerY / 3;
        mCrossPoint[2] = position;

        position = new Point();
        position.x = centerX + centerX / 3;
        position.y = centerY / 3 * 2;
        mCrossPoint[3] = position;
    }
加载成功的效果实现

  向外暴露方法 loadSucceed(),来实现加载成功的入口。加载完成后,先有个以画布中心为圆心的实心圆不断缩小的过渡动画,然后同时播放钩出现的动画和放大再回弹的动画。同样采用属性动画来实现。动画的实现,跟上面的加载条基本一致,也是利用 ValueAnimator 的值不断变化,然后不断重绘,来实现动画效果。

  1. 绘制向圆心缩的动画,先绘制一个固定的背景实心圆,然后通过 ValueAnimator 值的变化,在背景上绘制半径不断减小的实心圆。
  2. 绘制一个钩,只需要根据之前保存在 mTickPoint 数组中的三个点的坐标,通过 drawLine 绘制两条线即可。然后通过ValueAnimator 值变化,改变绘制的透明度,从全透明到不透明,免得钩出现的很突兀,有一个过渡效果。
  3. 放大再回弹的效果。通过 ObjectAnimator 的缩放动画来完成。先放大,再缩小到圆尺寸即可。

  动画完成后,如果需要进行进一步的逻辑操作,可以传入一个监听接口,当动画完成后,会回调 onAnimationEnd 方法,在该方法中进行相应操作即可。

    private ValueAnimator mCircleRadiusAnimator;
    private ValueAnimator mTickAnim;
    private AnimatorSet mAnimatorSet = new AnimatorSet();
    private ObjectAnimator mScaleAnimator;

    public void loadSucceed(@Nullable Animator.AnimatorListener listener) {
        clearAllAnimator();

        //初始化向圆心缩小的圆的动画
        mCircleRadiusAnimator = ValueAnimator.ofFloat(0, getWidth() / 2f);
        mCircleRadiusAnimator.setDuration(ANIMATOR_TIME / 2);

        //初始化打钩的动画,并注册监听
        mTickAnim = ValueAnimator.ofInt(0, 255);
        mTickAnim.setDuration(ANIMATOR_TIME / 2);
        if (listener != null) {
            mTickAnim.addListener(listener);
        }

        //放大再回弹的动画
        mScaleAnimator = getScaleAnimator();

        mAnimatorSet.play(mTickAnim).after(mCircleRadiusAnimator).with(mScaleAnimator);
        mAnimatorSet.start();
    }

    //获取放大再回弹的动画
    private ObjectAnimator getScaleAnimator() {
        PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat(SCALE_X, 1f, 1.2f, 1f);
        PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat(SCALE_Y, 1f, 1.2f, 1f);

        return ObjectAnimator
                .ofPropertyValuesHolder(this, scaleX, scaleY)
                .setDuration(ANIMATOR_TIME / 2);
    }

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

        //绘制向圆心缩小的圆
        if (mCircleRadiusAnimator != null && mCircleRadiusAnimator.isRunning()) {
            drawBackground(canvas, mLoadingBarColor);
            mPaintFill.setColor(mTickOrCrossColor);
            canvas.drawCircle(getWidth() / 2f, getHeight() / 2f,
                    getWidth() / 2f - (float) mCircleRadiusAnimator.getAnimatedValue(), mPaintFill);
            invalidate();
        }

        //绘制钩
        if (mTickAnim != null && mTickAnim.isRunning()) {
            drawBackground(canvas, mLoadingBarColor);
            mPaintStroke.setAlpha((int) mTickAnim.getAnimatedValue());
            mPaintStroke.setColor(mTickOrCrossColor);
            mPaintStroke.setStrokeCap(Paint.Cap.ROUND); //画线时,线头为圆形
            canvas.drawLine(mTickPoint[0].x, mTickPoint[0].y, mTickPoint[1].x, mTickPoint[1].y, mPaintStroke);
            canvas.drawLine(mTickPoint[1].x, mTickPoint[1].y, mTickPoint[2].x, mTickPoint[2].y, mPaintStroke);
            invalidate();
        }

        ......
    }
加载失败的效果实现

  向外暴露方法 loadFailed(),来实现加载失败的入口。大部分的实现和上面的一致。唯一的区别就是绘制叉。绘制叉也很简单,根据保存在数组 mCrossPoint 中的四个点的坐标,通过 drawLine 绘制出两条交叉的线即可完成。

    private ValueAnimator mCircleRadiusAnimator;
    private ValueAnimator mCrossAnim;
    private AnimatorSet mAnimatorSet = new AnimatorSet();
    private ObjectAnimator mScaleAnimator;

    public void loadFailed(@Nullable Animator.AnimatorListener listener) {
        clearAllAnimator();

        //初始化向圆心缩小的圆的动画
        mCircleRadiusAnimator = ValueAnimator.ofFloat(0, getWidth() / 2f);
        mCircleRadiusAnimator.setDuration(ANIMATOR_TIME / 2);

        //初始化打叉的动画,并注册监听
        mCrossAnim = ValueAnimator.ofInt(0, 255);
        mCrossAnim.setDuration(ANIMATOR_TIME / 2);
        if (listener != null) {
            mCrossAnim.addListener(listener);
        }

        //放大再回弹的动画
        mScaleAnimator = getScaleAnimator();

        mAnimatorSet.play(mCrossAnim).after(mCircleRadiusAnimator).with(mScaleAnimator);
        mAnimatorSet.start();
    }

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

        //绘制向圆心缩小的圆
        if (mCircleRadiusAnimator != null && mCircleRadiusAnimator.isRunning()) {
            drawBackground(canvas, mLoadingBarColor);
            mPaintFill.setColor(mTickOrCrossColor);
            canvas.drawCircle(getWidth() / 2f, getHeight() / 2f,
                    getWidth() / 2f - (float) mCircleRadiusAnimator.getAnimatedValue(), mPaintFill);
            invalidate();
        }

        //绘制叉
        if (mCrossAnim != null && mCrossAnim.isRunning()) {
            drawBackground(canvas, mLoadingBarColor);
            mPaintStroke.setAlpha((int) mCrossAnim.getAnimatedValue());
            mPaintStroke.setColor(mTickOrCrossColor);
            canvas.drawLine(mCrossPoint[0].x, mCrossPoint[0].y, mCrossPoint[2].x, mCrossPoint[2].y, mPaintStroke);
            canvas.drawLine(mCrossPoint[1].x, mCrossPoint[1].y, mCrossPoint[3].x, mCrossPoint[3].y, mPaintStroke);
            invalidate();
        }
    }

  啰嗦完毕,欢迎交流指教哦。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值