美文网首页Androidandroid项目
Android-打造一个简单通用的Material加载Loadi

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

作者: 三好码农 | 来源:发表于2015-05-07 22:32 被阅读16855次

    我们开发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

    相关文章

      网友评论

      • 666swb:github下载好,运行不是圆形loading,需要设置什么吗
      • mazhiman:这个该如何导入eclipse中呢,导入出错,不能运行?新手诚心求教,望回复。
      • 三好码农:@Hugetimor 谢谢你的建议,我尽快处理一下 :grin:

      本文标题:Android-打造一个简单通用的Material加载Loadi

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