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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏水击三千

ArcGIS for Android学习(一)

GIS的开发中,什么时候都少不了地图操作。ArcGIS for Android中,地图组件就是MapView,MapView是基于Android中ViewGro...

1.6K60
来自专栏老司机的简书

老司机带你走进Core Animation 之CAAnimation

开玩笑的,前段时间ipv6被拒啊,超级悲剧的,前后弄了好久,然后需求啊什么的又超多,所以写好的东西也没有时间整理。不过既然我现在回来了,那么这将是一个井喷的时节...

22720
来自专栏糊一笑

移动端效果之Picker

写在前面 接着前面的移动端效果的研究,这次来看看picker选择器的实现原理 移动端效果之Swiper 移动端效果之CellSwiper 移动端效果之Index...

43730
来自专栏青玉伏案

iOS开发之各种动画各种页面切面效果

注:其中有些效果调用了CATransition的Private API, 仅供娱乐。 补充:还是有好多小伙伴问那些可以在AppStore中使用,调用私有API的...

222100
来自专栏Java后端生活

Markdown的十个基本标签

51470
来自专栏GIS讲堂

修改selectToUISlider实现历史影像的对比与显示

2014年12月7日,星期日,天气,晴,是个好日子,闲来无事,将selectToUISlider与Arcgis for JS结合起来,做了一个类似于历史影像对比...

14930
来自专栏進无尽的文章

实践-跑马灯效果及实现过程解析

9710
来自专栏Alan's Lab

swift 写 iOS 空心字描边动画

http://oleb.net/blog/2010/12/animating-drawing-of-cgpath-with-cashapelayer/ http...

21030
来自专栏DeveWork

jQuery仿极客公园火箭发射“返回顶部”效果(优化篇)

承接上一篇《jQuery仿极客公园火箭发射“返回顶部”效果(初始篇)》,本文将对前一篇的代码进行优化。还是转载自andyliu: 先给出个演示Demo:演示地址...

23260
来自专栏快乐八哥

Cmd Markdown编辑器简明语法手册

标签: Cmd-Markdown 1. 斜体和粗体 使用 * 和 ** 表示斜体和粗体。 示例: 这是 斜体,这是 粗体。 2. 分级标题 使用 === 表示一...

21970

扫码关注云+社区

领取腾讯云代金券