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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容