我们开发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));
}
}
现在已经有了圆形指示器,还需要一个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