美文网首页Android 自定义view开发caseandroid之道
一个非常漂亮的自定义Loading,有加载成功和失败两种状态。

一个非常漂亮的自定义Loading,有加载成功和失败两种状态。

作者: hadisi5216 | 来源:发表于2016-07-14 10:54 被阅读4241次
    这还只是张图片

    本文原创,这篇可不能匿名转载。

    背景:我一哥们公司做智能设备的,该动画用在手机和家中网络连接时用,他让我看了下需求。刚看到这动画时感觉产品\UI设计的不错,想着试试。昨天开始做的,本来感觉很简单,但做起来貌似没那么简单;最后花了近一天时间终于搞定了。看看效果还行!

    niceloading.gif
    • 如果有想直接用的同道中人,看前半部分就行;如果想批评指正我的思考的看看后半部分

    1.直接上代码(NiceLoadingView)

    package com.hadisi.niceloading;
    
    import android.animation.ValueAnimator;
    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.graphics.Rect;
    import android.util.AttributeSet;
    import android.view.View;
    import android.view.animation.LinearInterpolator;
    
    /**
     * Created by hadisi5216 on 2016/7/12.
     */
    
    public class NiceLoadingView extends View {
    
        private Context mContext;
        private Paint mPaint;
    
        private int widthSpecSize;
        private int heightSpecSize;
        private int radiusSmall = 38;
        private int radiusbig = 76;
        private int moveX;
        private int XPoint;
    
        private int mState = -1;//0失败,1成功,-1默认
        private boolean mflag;
    
        private ValueAnimator animator;
    
        public NiceLoadingView(Context context) {
            super(context);
        }
    
        public NiceLoadingView(Context context, AttributeSet attrs) {
            super(context, attrs);
    
        }
    
        public NiceLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mContext = context;
            mPaint = new Paint();
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            mPaint.setColor(0xFFFFBC53);
            mPaint.setAntiAlias(true);
            if (Math.abs(moveX) > widthSpecSize * 5 / 4) {
                XPoint = (moveX < 0) ? XPoint = widthSpecSize * 7 / 4 - Math.abs(moveX) : widthSpecSize - widthSpecSize * 7 / 4 + Math.abs(moveX);
                canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
            }
            if (Math.abs(moveX) > widthSpecSize && Math.abs(moveX) < widthSpecSize * 3 / 2) {
                XPoint = (moveX < 0) ? XPoint = widthSpecSize * 3 / 2 - Math.abs(moveX) : widthSpecSize - widthSpecSize * 3 / 2 + Math.abs(moveX);
                canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
            }
            if (Math.abs(moveX) > widthSpecSize * 3 / 4 && Math.abs(moveX) < widthSpecSize * 5 / 4) {
                XPoint = (moveX < 0) ? XPoint = widthSpecSize * 5 / 4 - Math.abs(moveX) : widthSpecSize - widthSpecSize * 5 / 4 + Math.abs(moveX);
                canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
            }
            if (Math.abs(moveX) > widthSpecSize / 2 && Math.abs(moveX) < widthSpecSize) {
                XPoint = (moveX < 0) ? XPoint = widthSpecSize - Math.abs(moveX) : widthSpecSize - widthSpecSize + Math.abs(moveX);
                canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
            }
            if (Math.abs(moveX) > widthSpecSize / 4 && Math.abs(moveX) < widthSpecSize * 3 / 4) {
                XPoint = (moveX < 0) ? XPoint = widthSpecSize * 3 / 4 - Math.abs(moveX) : widthSpecSize - widthSpecSize * 3 / 4 + Math.abs(moveX);
                canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
            }
            if (Math.abs(moveX) > 0 && Math.abs(moveX) < widthSpecSize / 2) {
                XPoint = (moveX < 0) ? XPoint = widthSpecSize / 2 - Math.abs(moveX) : widthSpecSize - widthSpecSize / 2 + Math.abs(moveX);
                canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
            }
            //中间大圆
            if (Math.abs(moveX) > 0 && Math.abs(moveX) < widthSpecSize * 5 / 4) {
                radiusbig = 2 * radiusSmall - radiusSmall * (Math.abs(moveX)) / (widthSpecSize * 5 / 4);
                radiusbig = (radiusbig > radiusSmall) ? radiusbig : radiusSmall;
                canvas.drawCircle(widthSpecSize / 2, heightSpecSize / 2, radiusbig, mPaint);
            }
            if (Math.abs(moveX) < 12 && mState >= 0) {
                if (mState == 0) {
                    canvas.drawCircle(widthSpecSize / 2, heightSpecSize / 2, radiusbig, mPaint);
                    Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.connect_failed);
                    canvas.drawBitmap(bitmap, null, new Rect(widthSpecSize / 2 - radiusbig, heightSpecSize / 2 - radiusbig, widthSpecSize / 2 + radiusbig, heightSpecSize / 2 + radiusbig), mPaint);
                }
                if (mState == 1) {
                    canvas.drawCircle(widthSpecSize / 2, heightSpecSize / 2, radiusbig, mPaint);
                    Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.connect_success);
                    canvas.drawBitmap(bitmap, null, new Rect(widthSpecSize / 2 - radiusbig, heightSpecSize / 2 - radiusbig, widthSpecSize / 2 + radiusbig, heightSpecSize / 2 + radiusbig), mPaint);
                }
            }
        }
    
        public void start() {
            if (animator != null)
                animator.cancel();
            moveX = widthSpecSize * (-9 / 4);
            mState = -1;
            mflag = true;
            post(new Runnable() {
                @Override
                public void run() {
                    animator = ValueAnimator.ofFloat(0f, 1.0f);
                    animator.setRepeatMode(ValueAnimator.RESTART);
                    animator.setRepeatCount(ValueAnimator.INFINITE);
                    animator.setInterpolator(new LinearInterpolator());
                    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                            if (mState < 0) {
                                moveX = (moveX > widthSpecSize * 7 / 4) ? widthSpecSize * (-9 / 4) : moveX + 12;
                            } else {
                                if (moveX > 0)
                                    moveX = (moveX > widthSpecSize * 7 / 4) ? widthSpecSize * (-9 / 4) : moveX + 12;
                                else if (moveX < 0 && mflag) {
                                    moveX += 12;
                                    if (Math.abs(moveX) < 12)
                                        mflag = false;
                                }
                            }
                            postInvalidate();
                        }
                    });
                    animator.start();
                }
            });
        }
    
        public void success() {
            mState = 1;
        }
    
        public void failed() {
            mState = 0;
        }
    }
    

    项目已上传到github,戳着

    2.怎么用?

    • 布局文件中
    <com.hadisi.niceloading.NiceLoadingView
                android:id="@+id/nice_loading"
                android:layout_width="match_parent"
                android:layout_height="100dp" />
    
    • 你要用的地方
    NiceLoadingView niceLoading = (NiceLoadingView) findViewById(R.id.nice_loading);
    ……
    //开始连接时
    niceLoading.start();
    ……
    //连接成功时
    niceLoading.success();
    ……
    //连接失败时
    niceLoading.failed();
    

    3.我怎么实现的!

    仔细看效果图可以得出:
    1、有6个小圆依次从屏幕左侧移入屏幕中间,然后又依次从屏幕中间移出屏幕右侧。
    2、中间有个大圆在随着小圆的依次靠近慢慢变大,离开慢慢变小;注意在左侧第1个小圆到达中间时才出现大圆,在最后一个小圆准备向右侧移动时消失;大圆的半径在小圆半径和大圆半径之间。
    3、不管何时得到成功和失败的状态,动画终止时都是在小圆依次从左边进入中间后。
    4、动画完成后显示成功/失败图片和大圆。

    • 1. 6个小圆的运动
      我是这样想的:当第1个小圆移动到widthSpecSize/4(widthSpecSize 为控件的宽度)时第2个小圆开始移动、当第2个小圆移动到widthSpecSize/4 时第3个小圆开始移动......当第5个小圆移动到widthSpecSize/4 时第6个小圆开始移动、第6个小圆移动到widthSpecSize/2 时继续移动、当第6个小圆移动到widthSpecSize * 3/4时第5个小圆开始移动......当第2个小圆移动到widthSpecSize * 3/4时第1个小圆开始移动、最后第1个小球移出屏幕右侧,到此为一个循环。
      假设有一个位移变量moveX,moveX在不断增加,其变化范围为(a,b);可以看出按照我的想法,第1个小圆在范围的两边时开始移动、第6个小圆在变化范围的中间部分开始移动。
      我们可以继续假设变化范围为(-a,a),这样第1个小圆在范围的绝对值大时开始移动、第6个小圆在变化范围的绝对值小时开始移动;其实这种重复的动作很容易想到绝对值控制
      找张纸画下很容易得到moveX的变化范围在(-widthSpecSize * 7/4 , widthSpecSize * 7/4)之间。
    自己画的图,有点丑

    对照图很快可以得出下面代码

    if (Math.abs(moveX) > widthSpecSize * 5 / 4) {
        XPoint = (moveX < 0) ? XPoint = widthSpecSize * 7 / 4 - Math.abs(moveX) : widthSpecSize - widthSpecSize * 7 / 4 + Math.abs(moveX);
        canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
    }
    if (Math.abs(moveX) > widthSpecSize && Math.abs(moveX) < widthSpecSize * 3 / 2) {
        XPoint = (moveX < 0) ? XPoint = widthSpecSize * 3 / 2 - Math.abs(moveX) : widthSpecSize - widthSpecSize * 3 / 2 + Math.abs(moveX);
        canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
    }
    if (Math.abs(moveX) > widthSpecSize * 3 / 4 && Math.abs(moveX) < widthSpecSize * 5 / 4) {
        XPoint = (moveX < 0) ? XPoint = widthSpecSize * 5 / 4 - Math.abs(moveX) : widthSpecSize - widthSpecSize * 5 / 4 + Math.abs(moveX);
        canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
    }
    if (Math.abs(moveX) > widthSpecSize / 2 && Math.abs(moveX) < widthSpecSize) {
        XPoint = (moveX < 0) ? XPoint = widthSpecSize - Math.abs(moveX) : widthSpecSize - widthSpecSize + Math.abs(moveX);
        canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
    }
    if (Math.abs(moveX) > widthSpecSize / 4 && Math.abs(moveX) < widthSpecSize * 3 / 4) {
        XPoint = (moveX < 0) ? XPoint = widthSpecSize * 3 / 4 - Math.abs(moveX) : widthSpecSize - widthSpecSize * 3 / 4 + Math.abs(moveX);
        canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
    }
    if (Math.abs(moveX) > 0 && Math.abs(moveX) < widthSpecSize / 2) {
        XPoint = (moveX < 0) ? XPoint = widthSpecSize / 2 - Math.abs(moveX) : widthSpecSize - widthSpecSize / 2 + Math.abs(moveX);
        canvas.drawCircle(XPoint, heightSpecSize / 2, radiusSmall, mPaint);
    }
    
    • 2. 中间大圆的运动
      中间变化的大圆在左侧第1个小圆到达中间时才出现大圆,在第1个小圆准备向右侧移动时消失,变化范围(-widthSpecSize * 5/4 , widthSpecSize * 5/4)之间。大圆的半径在小圆半径和大圆半径之间,我们用radiusbig = radiusbig - radiusSmall * (Math.abs(moveX)) / (widthSpecSize * 5/4)计算大圆半径,可以得到慢慢变大和变小的效果,然后控制在小于radiusSmall时用radiusSmall。
    if (Math.abs(moveX) > 0 && Math.abs(moveX) < widthSpecSize * 5 / 4) {
        radiusbig = radiusbig - radiusSmall * (Math.abs(moveX)) / (widthSpecSize * 5 / 4);
        radiusbig = (radiusbig > radiusSmall) ? radiusbig : radiusSmall;
        canvas.drawCircle(widthSpecSize / 2, heightSpecSize / 2, radiusbig, mPaint);
    }
    
    • 3. 动画终止的控制
      正常当一个循环结束时我们需要重新给moveX赋值为widthSpecSize * (-7/4),当收到成功或失败状态时需要判断当前的状态,等到动画进行到结束状态(小圆依次从左边进入中间后)。见下面代码,mState为当前状态(0失败,1成功,-1默认)。
      我重新赋值时将moveX设为 * widthSpecSize * (-9/4)因为一个循环结束后有点停顿会感觉舒服点,这个无所谓,自己感觉而已*
    if (mState < 0) {
        moveX = (moveX > widthSpecSize * 7 / 4) ? widthSpecSize * (-9 / 4) : moveX + 12;
    } else {
        if (moveX > 0)
            moveX = (moveX > widthSpecSize * 7 / 4) ? widthSpecSize * (-9 / 4) : moveX + 12;
        else if (moveX < 0 && mflag) {
            moveX += 12;
            if (Math.abs(moveX) < 12)
                mflag = false;
        }
    }
    
    • 4. 显示成功/失败图片
      这个简单,在收到成功或失败状态,待动画完成时先画一个大圆,再画一个bitmap
    if (Math.abs(moveX) < 12 && mState >= 0) {
                if (mState == 0) {
                    canvas.drawCircle(widthSpecSize / 2, heightSpecSize / 2, radiusbig, mPaint);
                    Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.connect_failed);
                    canvas.drawBitmap(bitmap, null, new Rect(widthSpecSize / 2 - radiusbig, heightSpecSize / 2 - radiusbig, widthSpecSize / 2 + radiusbig, heightSpecSize / 2 + radiusbig), mPaint);
                }
                if (mState == 1) {
                    canvas.drawCircle(widthSpecSize / 2, heightSpecSize / 2, radiusbig, mPaint);
                    Bitmap bitmap = BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.connect_success);
                    canvas.drawBitmap(bitmap, null, new Rect(widthSpecSize / 2 - radiusbig, heightSpecSize / 2 - radiusbig, widthSpecSize / 2 + radiusbig, heightSpecSize / 2 + radiusbig), mPaint);
                }
            }
    

    4.优化

    • 可以优化,将paint的颜色等属性、大小圆的半径、优化画小圆的逻辑,使小圆个数可变等抽象出来..........

    其实核心的就是想法,随便怎么优化。反正我就弄到这了,油而不腻,我觉得挺好,不需要太多优化。吼吼....

    相关文章

      网友评论

      • c0565131c680:用postInvalidateDelayed刷新好一点,控制在每秒30帧。用postInvalidate太耗GPU了
        hadisi5216: @c0565131c680 没事,不在乎那点GPU
      • 奋斗的Leo:你们的Ui是从dribbble上借鉴的吗?
        我也实现了类似的效果,不过我加了贝塞尔曲线去绘制粘性的效果.
        http://www.jianshu.com/p/4a022f9bb121
        奋斗的Leo:@hadisi5216 sogo
        hadisi5216:@奋斗的Leo 刚开始背景里我介绍过,是一哥们公司的需求。
      • 代号027:不错呦,不知道你用不用google+,你也可以借鉴!
      • 那年23:挺好的设计
      • code_间特门:虽然不是我想要的,但是为你的编程思想点赞,没准可以衍生出其他的需求。
        hadisi5216: @code_间特门 哇哦,谢谢
      • 捡淑:mark
      • 8351ae8a3b5e:那5,6个if感觉可以抽出方法?
        8351ae8a3b5e:@hadisi5216 嘛,变量只有XPoint,根据条件返回Xpoint,再调用drawCircle(),不需要六个显示调用吧~感觉可读性会高点。嘛 个人观点而已 :blush:
        hadisi5216:@rainboweast 嗯。我后面写了,可以使用for循环,将小圆的个数抽象出来;不过我觉得6个挺好,还有如果写成for循环看起来更费劲。“油而不腻”,我觉得这样挺好,个人感觉吧!
      • SZhua:标记一下,上班看
        hadisi5216:@SZhua 有眼光!
      • nbpzjy:很厉害
        hadisi5216:@nbpzjy 过奖了!谢谢
      • bug体质:mark
        bug体质:@hadisi5216 你好萌 居然一个个都回复了耶
        hadisi5216:@花叔叔 记得看啊
      • 09d8e043ac44:谢谢分享。
        hadisi5216:@节操君你在哪 你喜欢就行!
      • 帅气的昵称被占用:马克
        hadisi5216:@帅气的昵称被占用 记得看啊
      • 龙祈:……好长,m住慢慢看
        hadisi5216: @晏岚 不长,忽略代码没多少,再说我还一个个敲的
      • glumes:后半部分值得学习
        hadisi5216: @蓄意碎碎 我看好你哦

      本文标题:一个非常漂亮的自定义Loading,有加载成功和失败两种状态。

      本文链接:https://www.haomeiwen.com/subject/gnyejttx.html