我們開發App時,都難免要向服務器請求數據,在數據返回之前一般都需要有個進度指示器來告訴用戶,程序正在拼命幫你加載,當數據返回后展示正常數據,這是個很簡單也很常用的功能,但是可能每一個頁面都需要為這個簡單功能浪費精力體力,所以我們需要一個簡單通用的加載LoadingView。
實現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,共同進步.
最后是運行效果圖,有圖有真相。