Android-打造一个简单通用的Material加载LoadingView

我们开发App时,都难免要向服务器请求数据,在数据返回之前一般都需要有个进度指示器来告诉用户,程序正在拼命帮你加载,当数据返回后展示正常数据,这是个很简单也很常用的功能,但是可能每一个页面都需要为这个简单功能浪费精力体力,所以我们需要一个简单通用的加载LoadingView。

实现Material Progressbar

因为网络请求的时间一般是未知的,所以我们一般都是用一个循环的圆圈指示器来提示用户,如下图。


Material-Progressbar

这个View,仔细观察,可以按下面的步骤做无限循环来显示:

1.根据起始弧度startArc和要画的弧度arc,画一个弧形,弧度arc逐渐加大。
2.判读弧度arc是否大于maxArc,如果为真,起始弧度startArc开始增加,弧度arc逐渐减少。
3.当弧度arc小于minArc时,回到第1步。
同时,整个画布canvas在按照一个角速度做旋转。除此之外还有一件事情要做,需要在弧形中间画一个圆形,来擦除中间部分的颜色,我们可以用Xfermode来实现,Xfermode可以对多个图层按规则进行混合,具体可以自行Google哦。

我们开始动手实现,篇幅关系,只贴一些关键代码片段(项目已经共享到Github,结尾会给出链接)。

public class MaterialCircleView extends View {

/**
 * 是否需要对画笔颜色进行渐变处理
 */
private boolean bGradient;
/**
 * 画笔颜色
 */
private int circleColor;
/**
 * 画圆圈宽度
 */
private int circleWidth;
/**
 * 圆圈半径
 */
private int radius;
public MaterialCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    TypedArray t = null;
    try {
        t = context.obtainStyledAttributes(attrs, R.styleable.MaterialCircleView,
                0, defStyleAttr);
        setbGradient(t.getBoolean(R.styleable.MaterialCircleView_bGradient, true));
        circleColor = t.getColor(R.styleable.MaterialCircleView_circleColor,
                getResources().getColor(android.R.color.holo_blue_light));
        circleWidth = t.getDimensionPixelSize(R.styleable.MaterialCircleView_circleWidth,
                10);
        radius = t.getDimensionPixelSize(R.styleable.MaterialCircleView_radius,
                50);
    } finally {
        if (t != null) {
            t.recycle();
        }
    }

    mPaint = new Paint();
    if (isbGradient()) {
        mPaint.setColor(Color.rgb(red, green, blue));
    }else {
        mPaint.setColor(circleColor);
    }
    mPaint.setAntiAlias(true);
    setBackgroundColor(getResources().getColor(android.R.color.transparent));
}

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

    sWidth = this.getMeasuredWidth();
    sHeight = this.getMeasuredHeight();
    halfWidth = sWidth / 2;
    halfHeight = sHeight / 2;
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //计算startAngle和endAngle,
    //保证它们在maxAngle和minAngle之间循环递增递减
    if (startAngle == minAngle) {
        endAngle += 6;
    }
    if (endAngle >= 280 || startAngle > minAngle) {
        startAngle += 6;
        if(endAngle > 20) {
            endAngle -= 6;
        }

    }
    if (startAngle > minAngle + 280) {
        minAngle = startAngle;
        startAngle = minAngle;
        endAngle = 20;
    }

    checkPaint();
    //旋转canvas
    canvas.rotate(curAngle += rotateDelta, halfWidth, halfHeight);
    //将弧度和擦除圆形绘制在bitmap上
    Bitmap bitmap = Bitmap.createBitmap(sWidth, sHeight, Bitmap.Config.ARGB_8888);
    Canvas bmpCanvas = new Canvas(bitmap);
    bmpCanvas.drawArc(new RectF(0, 0, sWidth, sHeight), startAngle, endAngle, true, mPaint);
    Paint transparentPaint = new Paint();
    transparentPaint.setAntiAlias(true);
    transparentPaint.setColor(getResources().getColor(android.R.color.transparent));
    transparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
    bmpCanvas.drawCircle(halfWidth, halfHeight,
            halfWidth - circleWidth, transparentPaint);
    canvas.drawBitmap(bitmap, 0, 0, new Paint());
    //保证绘制动画延续
    invalidate();
}

整个实现过程就是这样,代码量比较少,这里顺带提一下,我们额外实现了一个颜色渐变的过程,R.styleable.MaterialCircleView_bGradient属性是true时启用,其实就一直改变mPaint的颜色。

private int colorDelta = 2;
private void checkPaint() {
    if (isbGradient()) {
        switch (phase % 5) {
            case 0:
                green += colorDelta;
                if (green > 255) {
                    green = 255;
                    phase ++;
                }
                break;
            case 1:
                red += colorDelta;
                green -= colorDelta;
                if (red > 255) {
                    red = 255;
                    green = 0;
                    phase ++;
                }
                break;
            case 2:
                blue -= colorDelta;
                if (blue < 0) {
                    blue = 0;
                    phase ++;
                }
                break;
            case 3:
                red -= colorDelta;
                green += colorDelta;
                if (red < 0) {
                    red = 0;
                    green = 255;
                    phase ++;
                }
                break;
            case 4:
                green -= colorDelta;
                blue += colorDelta;
                if (green < 0) {
                    green = 0;
                    blue = 255;
                    phase ++;
                }
                break;
        }
        mPaint.setColor(Color.rgb(red, green, blue));
    }
}

实现UniversalLoadingView

现在已经有了圆形指示器,还需要一个textView来显示文字,所以我们再封装一个ViewGroup,来管理加载的几种状态,包括指示器的隐藏和现实,textView文本的改变等。同样只贴关键代码片段。

public class UniversalLoadingView extends ViewGroup{
public enum State{
    GONE,
    LOADING,
    LOADING_FALIED,
    LOADING_EMPTY
}

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

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

public UniversalLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    TypedArray t = null;
    try {
        t = context.obtainStyledAttributes(attrs, R.styleable.MaterialCircleView,
                0, defStyleAttr);
        bGradient = t.getBoolean(R.styleable.MaterialCircleView_bGradient, true);
        circleColor = t.getColor(R.styleable.MaterialCircleView_circleColor,
                getResources().getColor(android.R.color.holo_blue_light));
        circleWidth = t.getDimensionPixelSize(R.styleable.MaterialCircleView_circleWidth,
                10);
        radius = t.getDimensionPixelSize(R.styleable.MaterialCircleView_radius,
                MaterialCircleView.dpToPx(50, getResources()));
    } finally {
        if (t != null) {
            t.recycle();
        }
    }

    try {
        t = context.obtainStyledAttributes(attrs, R.styleable.UniversalLoadingView,
                0, defStyleAttr);
        setbTransparent(t.getBoolean(R.styleable.UniversalLoadingView_bg_transparent, false));
        alpha = t.getDimensionPixelSize(R.styleable.UniversalLoadingView_bg_alpha,
                255);
    } finally {
        if (t != null) {
            t.recycle();
        }
    }

    materialCircleView = new MaterialCircleView(context, attrs, defStyleAttr);
   //add circle view
    addView(materialCircleView);

    mTipTextView = new TextView(context);
    mTipTextView.setText(LOADING_TIP);
    mTipTextView.setTextSize(16f);
    mTipTextView.setGravity(Gravity.CENTER);
    mTipTextView.setSingleLine(false);
    mTipTextView.setMaxLines(2);
    mTipTextView.setTextColor(getResources().getColo r(android.R.color.darker_gray));
    addView(mTipTextView);

    this.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {
            if (mLoadState == State.LOADING_EMPTY || mLoadState == State.LOADING_FALIED) {
                if (mReloadListener != null) {
                    mReloadListener.reload();
                }
            }
        }
    });

    mHandler = new Handler();

    if (isbTransparent()) {
        setBackgroundColor(getResources().getColor(android.R.color.transparent));
    }
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//        return super.onInterceptTouchEvent(ev);
    return true;
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    LayoutParams params = (LayoutParams) materialCircleView.getLayoutParams();
    sWidth = MeasureSpec.getSize(widthMeasureSpec);
    sHeight = MeasureSpec.getSize(heightMeasureSpec);
    params.left = (sWidth - radius) / 2;
    params.top = (sHeight - radius) / 2 - radius;
    params.width = radius;
    params.height = radius;

    LayoutParams tipParams = (LayoutParams) mTipTextView.getLayoutParams();
    int tipWidth = MaterialCircleView.dpToPx(100, getResources());
    int tipHeight = MaterialCircleView.dpToPx(50, getResources());
    tipParams.left = (sWidth - tipWidth) / 2;
    tipParams.top = (sHeight - radius) / 2 ;
    tipParams.width = tipWidth;
    tipParams.height = tipHeight;

    setMeasuredDimension(sWidth, sHeight);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    LayoutParams params = (LayoutParams) materialCircleView.getLayoutParams();
    materialCircleView.layout(params.left, params.top, params.left + params.width
                , params.top + params.height);
    LayoutParams tipParams = (LayoutParams) mTipTextView.getLayoutParams();
    mTipTextView.layout(tipParams.left, tipParams.top, tipParams.left + tipParams.width,
            tipParams.top + tipParams.height);
}

我们还需要一个暴露一个重试加载数据的接口,因为总有网络不好的时候。

public void setOnReloadListener(ReloadListner listener) {
    this.mReloadListener = listener;
}

/**
 * reload interface
 */
public interface ReloadListner {
    public void reload();
}

在Activity的Xml布局文件中,我们可以直接添加

  <com.sw.library.widget.library.UniversalLoadingView
    android:id="@+id/loadingView"
    app:bGradient="false"
    app:radius="50dp"
    app:bg_transparent="false"
    app:circleColor="@android:color/holo_green_dark"
    android:background="@android:color/white"
    android:layout_width="match_parent"
    android:layout_height="match_parent"></com.sw.library.widget.library.UniversalLoadingView>

也可以直接new UniversalLoadingView来创建,然后addView到布局根容器中。
这个项目我已经共享到Github了 https://github.com/aliouswang/UniversalLoadingView
现在功能还比较弱,还有很多地方可以改进,欢迎大家pull request,共同进步.
最后是运行效果图,有图有真相。

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

推荐阅读更多精彩内容