寫自定義view,怎麼能沒有自己寫的loading框了,今天給大家送上一個loading框,前期losing框是很簡單的效果,三個狀態:loading、success、error,不逼逼了,先上給糙一點的效果GIF,回頭繼續完善。
糙一點的loading框效果看完了,接下來說一下實現過程,採用WindowManager將losing框的view置於顯示view的上層,起初上來是loading轉圈狀態,根據載入返回的結構設定成功或者失敗。
loading狀態
ValueAnimator實現AnimatorUpdateListener、AnimatorListener監聽方法,AnimatorUpdateListener中接受valueAnimator.getAnimatedValue();(不斷回撥的在0-1這個範圍內,經過插值器插值之後的返回值),然後去畫圈圈
這裡畫的是一個通過改變不同起始角度畫給一個240度的圓弧,以此達到loading效果
mPath = new Path();
loadingrectF = new RectF(-100, -100, 100, 100);
mPath.addArc(loadingrectF, mAnimatorValue * 360, 240);
canvas.drawPath(mPath, mPaint);
canvas.drawColor(Color.parseColor("#33000000"));複製程式碼
mAnimatorValue值的變化是AnimatorUpdateListener函式裡面回撥的值,然後通知重繪
loadingvalueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// 不斷回撥的在0-1這個範圍內,經過插值器插值之後的返回值
mAnimatorValue = (float) valueAnimator.getAnimatedValue();
//重繪
invalidate();
}
});複製程式碼
為了保證轉了一圈過後繼續轉,這裡引進Handler,在AnimatorListener的結束方法中進行通知動畫繼續
loadingvalueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
// getHandle發訊息通知動畫狀態更新
mAnimatorHandler.sendEmptyMessage(0);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});複製程式碼
附上Handler相關程式碼
mAnimatorHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (mcurrentState) {
case LODING:
//保持loading時一直執行動畫
loadingvalueAnimator.start();
break;
case SUCCES:
break;
case ERROR:
break;
default:
break;
}
}
};複製程式碼
到此為止,loading就能一直轉了,接下來就是success、error兩個狀態了的講解了
success狀態
成功狀態時,loading消失,顯示成功的相關提醒(原諒我是一個程式設計師,做的loading不好看,準備後期功能完善後,會把loading、success、error狀態的顯示弄的像UI設計的一樣),廢話不多說了,該上菜了
/**
* 成功
*/
public void setSuccess() {
loadingvalueAnimator.removeAllListeners();
errorvalueAnimator.removeAllListeners();
addSuccesLoadingListener();
successvalueAnimator.start();
}複製程式碼
移除掉其他ValueAnimator對應的監聽方法,為success加上ValueAnimator的監聽方法,這裡就是顯示一下success狀態的文案,保持500ms,然後顯示(這一塊可以改成其他動畫酷炫效果)
successvalueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
//動畫開始,去繪製success的文案
mcurrentState = State.SUCCES;
invalidate();
}
@Override
public void onAnimationEnd(Animator animator) {
//動畫結束,隱藏狀態
LoadingView.this.setVisibility(GONE);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});複製程式碼
error狀態
和success狀態同理,只是文案不一樣
程式碼中使用
新增到activity開始loading動畫,待結果返回時然後設定相關狀態顯示500ms,然後顯示消失,接下來貼出模擬的loading--->success--->dismiss的一個過程
loadingRoundView = new LoadingView(this);
loadingRoundView.addPartentViewStartLoading(MainActivity.this);
mErrorHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (error <= 50) {
if (error == 0) {
loadingRoundView.startLoading();
}
error += 1;
mErrorHandler.sendEmptyMessageDelayed(0, 100);
} else {
loadingRoundView.setError();
}
}
};
//
mSuccesHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (succes <= 50) {
succes += 1;
mSuccesHandler.sendEmptyMessageDelayed(0, 100);
} else {
loadingRoundView.setSuccess();
mErrorHandler.sendEmptyMessageDelayed(0, 1000);
}
}
};
mSuccesHandler.sendEmptyMessageDelayed(0, 100);複製程式碼
設定loading方式
目前只寫了三個loading方式
//loading的型別
private Type mcurrentType = Type.ROUND;
//loading type
public enum Type {
ARC,//傳統弧形轉圈
CIRCLE,//天女散花
ROUND,//漸變的圓圈旋轉
}複製程式碼
然後根據不同的loading方法,ondraw()裡面執行的方法也不一樣
case LODING:
if (mcurrentType == Type.ARC) {
mPath = new Path();
RectF loadingrectF = new RectF(-radius, -radius, radius, radius);
mPath.addArc(loadingrectF, mAnimatorValue * 360, 240);
canvas.drawPath(mPath, mPaint);
} else if (mcurrentType == Type.CIRCLE) {
mPaint.setStyle(Paint.Style.FILL);
mPath = new Path();
for (int i = 0; i < 10; i++) {
mPath.addCircle(mAnimatorValue * mwidth / 2 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 2 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
mPath.addCircle(mAnimatorValue * mwidth / 3 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 3 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
mPath.addCircle(mAnimatorValue * mwidth / 4 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 4 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
mPath.addCircle(mAnimatorValue * mwidth / 5 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 5 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
mPath.addCircle(mAnimatorValue * mwidth / 6 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 6 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
}
canvas.drawPath(mPath, mPaint);
} else if (mcurrentType == Type.ROUND) {
mPaint.setStyle(Paint.Style.FILL);
Path path = new Path();
path.addCircle(0, 0, radius, Path.Direction.CW); // 新增一個圓形
PathMeasure pathMeasure = new PathMeasure(path, false);
pathMeasure.getPosTan(pathMeasure.getLength() * mAnimatorValue, pos, tan);
// mPath = new Path();
//使用 Math.atan2(tan[1], tan[0]) 將 tan 轉化為角(單位為弧度)的時候要注意引數順序。
float angle = (float) Math.atan2(tan[1], tan[0]);
for (int i = 0; i < roundCount; i++) {
//用path一次性畫的,透明度不好設定
// mPath.addCircle((float) (Math.cos(angle + i*0.4) * 100), (float) (Math.sin(angle+ i*0.4) * 100), 16, Path.Direction.CW);
mPaint.setAlpha(onealpha * i);
canvas.drawCircle((float) (Math.cos(angle + i * 0.4) * radius), (float) (Math.sin(angle + i * 0.4) * radius), circleRadius, mPaint);
}
// canvas.drawPath(mPath, circlePaint);
}
break;複製程式碼
附上loadingview完整程式碼
package com.loadingview;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
/**
* Created by wujun on 2017/7/31.
*
* @author madreain
* @desc loading框 目前支援三種狀態及三種樣式
*/
public class LoadingView extends View {
//螢幕的寬高
int mwidth;
int mheight;
int backgroudColor;
//主
Paint mPaint;
//屬性
int mpaintColor;
float mpaintStrokeWidth;
Path mPath;
//轉圈的半徑
float radius = 100;
//畫圓圈的半徑
float circleRadius = 16;
//設定轉圈的圓點數量
private int roundCount = 10;
//1-255
private int onealpha;
private float[] pos; // 當前點的實際位置
private float[] tan; // 當前點的tangent值,用於計算圖片所需旋轉的角度
//loading的動效
ValueAnimator loadingvalueAnimator;
//loading 動畫數值(用於控制動畫狀態,因為同一時間內只允許有一種狀態出現,具體數值處理取決於當前狀態)
private float mAnimatorValue = 0;
//loading 用於控制動畫狀態轉換
private Handler mAnimatorHandler;
State mcurrentState = State.LODING;
//loading狀態,loading,成功,失敗
private enum State {
LODING,
SUCCES,
ERROR,
}
//成功失敗的畫筆
Paint textPaint;
int textPaintColor;
float textPaintStrokeWidth;
float textPaintTextSize;
//成功的動效
ValueAnimator successvalueAnimator;
//失敗的動效
ValueAnimator errorvalueAnimator;
//loading的型別
private Type mcurrentType = Type.ROUND;
//loading type
public enum Type {
ARC,//傳統弧形轉圈
CIRCLE,//天女散花
ROUND,//漸變的圓圈旋轉
}
public LoadingView(Context context) {
super(context);
initPaint();
initHandler();
initListener();
}
public LoadingView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initTypedArray(context, attrs);
initPaint();
initHandler();
initListener();
}
private void initTypedArray(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
backgroudColor=typedArray.getColor(R.styleable.LoadingView_backgroudColor,Color.parseColor("#66000000"));
mpaintColor = typedArray.getColor(R.styleable.LoadingView_mpaintColor, Color.BLUE);
mpaintStrokeWidth = typedArray.getFloat(R.styleable.LoadingView_mpaintStrokeWidth, 16);
textPaintColor = typedArray.getColor(R.styleable.LoadingView_textPaintColor, Color.BLUE);
textPaintStrokeWidth = typedArray.getFloat(R.styleable.LoadingView_textPaintStrokeWidth, 6);
textPaintTextSize = typedArray.getFloat(R.styleable.LoadingView_textPaintTextSize, 60);
radius = typedArray.getFloat(R.styleable.LoadingView_radius, 100);
circleRadius = typedArray.getFloat(R.styleable.LoadingView_circleRadius, 16);
roundCount = typedArray.getInteger(R.styleable.LoadingView_roundCount, 10);
int type = typedArray.getInt(R.styleable.LoadingView_Type, 0);
if (type == 0) {
mcurrentType = Type.ARC;
} else if (type == 1) {
mcurrentType = Type.CIRCLE;
} else if (type == 2) {
mcurrentType = Type.ROUND;
}
}
public LoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initTypedArray(context, attrs);
initPaint();
initHandler();
initListener();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mwidth = w;
mheight = h;
}
private void initPaint() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(mpaintColor);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(mpaintStrokeWidth);
mPaint.setStyle(Paint.Style.STROKE);
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor(textPaintColor);
textPaint.setStrokeCap(Paint.Cap.ROUND);
textPaint.setStrokeWidth(textPaintStrokeWidth);
textPaint.setStyle(Paint.Style.STROKE);
textPaint.setTextSize(textPaintTextSize);
pos = new float[2];
tan = new float[2];
//計算透明度
onealpha = 255 / roundCount;
}
private void initHandler() {
mAnimatorHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (mcurrentState) {
case LODING:
//保持loading時一直執行動畫
loadingvalueAnimator.start();
break;
case SUCCES:
break;
case ERROR:
break;
default:
break;
}
}
};
}
private void initListener() {
// 建立0-1的一個過程,任何複雜的過程都可以採用歸一化,然後在addUpdateListener回撥裡去做自己想要的變化
loadingvalueAnimator = ValueAnimator.ofFloat(0, 1);
// 設定過程的時間為2S
loadingvalueAnimator.setDuration(2000);
successvalueAnimator = ValueAnimator.ofFloat(0, 1);
successvalueAnimator.setDuration(500);
errorvalueAnimator = ValueAnimator.ofFloat(0, 1);
errorvalueAnimator.setDuration(500);
}
private void addLoadingListener() {
loadingvalueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// 不斷回撥的在0-1這個範圍內,經過插值器插值之後的返回值
mAnimatorValue = (float) valueAnimator.getAnimatedValue();
// 獲取當前點座標封裝到mCurrentPosition
// mPathMeasure.getPosTan(mAnimatorValue, mCurrentPosition, null);
//重繪
invalidate();
}
});
loadingvalueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
// getHandle發訊息通知動畫狀態更新
mAnimatorHandler.sendEmptyMessage(0);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
private void addSuccesLoadingListener() {
// successvalueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
// @Override
// public void onAnimationUpdate(ValueAnimator valueAnimator) {
//
//
// }
// });
successvalueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
mcurrentState = State.SUCCES;
invalidate();
}
@Override
public void onAnimationEnd(Animator animator) {
//動畫結束,隱藏狀態
LoadingView.this.setVisibility(GONE);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
private void addErrorLoadingListener() {
// errorvalueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
// @Override
// public void onAnimationUpdate(ValueAnimator valueAnimator) {
//
//
// }
// });
errorvalueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
mcurrentState = State.ERROR;
invalidate();
}
@Override
public void onAnimationEnd(Animator animator) {
//動畫結束,隱藏狀態
LoadingView.this.setVisibility(GONE);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//移到螢幕中間
canvas.translate(mwidth / 2, mheight / 2);
//都新增背景
canvas.drawColor(backgroudColor);
drawLoading(canvas);
}
private void drawLoading(Canvas canvas) {
switch (mcurrentState) {
case LODING:
if (mcurrentType == Type.ARC) {
mPath = new Path();
RectF loadingrectF = new RectF(-radius, -radius, radius, radius);
mPath.addArc(loadingrectF, mAnimatorValue * 360, 240);
canvas.drawPath(mPath, mPaint);
} else if (mcurrentType == Type.CIRCLE) {
mPaint.setStyle(Paint.Style.FILL);
mPath = new Path();
for (int i = 0; i < 10; i++) {
mPath.addCircle(mAnimatorValue * mwidth / 2 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 2 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
mPath.addCircle(mAnimatorValue * mwidth / 3 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 3 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
mPath.addCircle(mAnimatorValue * mwidth / 4 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 4 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
mPath.addCircle(mAnimatorValue * mwidth / 5 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 5 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
mPath.addCircle(mAnimatorValue * mwidth / 6 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 6 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
}
canvas.drawPath(mPath, mPaint);
} else if (mcurrentType == Type.ROUND) {
mPaint.setStyle(Paint.Style.FILL);
Path path = new Path();
path.addCircle(0, 0, radius, Path.Direction.CW); // 新增一個圓形
PathMeasure pathMeasure = new PathMeasure(path, false);
pathMeasure.getPosTan(pathMeasure.getLength() * mAnimatorValue, pos, tan);
// mPath = new Path();
//使用 Math.atan2(tan[1], tan[0]) 將 tan 轉化為角(單位為弧度)的時候要注意引數順序。
float angle = (float) Math.atan2(tan[1], tan[0]);
for (int i = 0; i < roundCount; i++) {
//用path一次性畫的,透明度不好設定
// mPath.addCircle((float) (Math.cos(angle + i*0.4) * 100), (float) (Math.sin(angle+ i*0.4) * 100), 16, Path.Direction.CW);
mPaint.setAlpha(onealpha * i);
canvas.drawCircle((float) (Math.cos(angle + i * 0.4) * radius), (float) (Math.sin(angle + i * 0.4) * radius), circleRadius, mPaint);
}
// canvas.drawPath(mPath, circlePaint);
}
break;
case SUCCES:
canvas.drawCircle(0, 0, radius, textPaint);
canvas.drawLine(-radius / 2, 0, 0, radius / 2, textPaint);
canvas.drawLine(0, radius / 2, radius / 2, -radius / 2, textPaint);
break;
case ERROR:
canvas.drawCircle(0, 0, radius, textPaint);
canvas.drawLine(radius / 2, radius / 2, -radius / 2, -radius / 2, textPaint);
canvas.drawLine(radius / 2, -radius / 2, -radius / 2, radius / 2, textPaint);
break;
default:
break;
}
}
/**
* 新增到activity的上層並執行動畫
* @param activity
*
*/
public void addPartentViewStartLoading(Activity activity) {
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
//Activity中View佈局的最祖宗佈局,是一個FrameLayout,叫做DecorView,通過getWindow().getDecorView()可以獲取到
FrameLayout view = (FrameLayout) activity.getWindow().getDecorView();
view.addView(this,layoutParams);
startLoading();
}
/**
* 設定loading開始
*/
public void startLoading() {
if (loadingvalueAnimator != null) {
mcurrentState = State.LODING;
addLoadingListener();
successvalueAnimator.removeAllListeners();
errorvalueAnimator.removeAllListeners();
//如果是gone --->VISIBLE
if (this.getVisibility() == GONE) {
this.setVisibility(VISIBLE);
}
loadingvalueAnimator.start();
}
}
/**
* 成功
*/
public void setSuccess() {
loadingvalueAnimator.removeAllListeners();
errorvalueAnimator.removeAllListeners();
addSuccesLoadingListener();
successvalueAnimator.start();
}
/**
* 失敗
*/
public void setError() {
loadingvalueAnimator.removeAllListeners();
successvalueAnimator.removeAllListeners();
addErrorLoadingListener();
errorvalueAnimator.start();
}
/**
* 整個事件的消費來保證loading狀態不可操作
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
/**
* 設定loading的背景顏色
* @param backgroudColor
*/
public void setBackgroudColor(int backgroudColor) {
this.backgroudColor = backgroudColor;
}
/**
*
* @param mpaintColor
*/
public void setMpaintColor(int mpaintColor) {
this.mpaintColor = mpaintColor;
}
/**
*
* @param mpaintStrokeWidth
*/
public void setMpaintStrokeWidth(float mpaintStrokeWidth) {
this.mpaintStrokeWidth = mpaintStrokeWidth;
}
/**
*
* @param textPaintColor
*/
public void setTextPaintColor(int textPaintColor) {
this.textPaintColor = textPaintColor;
}
/**
*
* @param textPaintStrokeWidth
*/
public void setTextPaintStrokeWidth(float textPaintStrokeWidth) {
this.textPaintStrokeWidth = textPaintStrokeWidth;
}
/**
*
* @param textPaintTextSize
*/
public void setTextPaintTextSize(float textPaintTextSize) {
this.textPaintTextSize = textPaintTextSize;
}
/**
*
* @param radius
*/
public void setRadius(float radius) {
this.radius = radius;
}
/**
*
* @param circleRadius
*/
public void setCircleRadius(float circleRadius) {
this.circleRadius = circleRadius;
}
/**
*
* @param roundCount
*/
public void setRoundCount(int roundCount) {
this.roundCount = roundCount;
}
/**
*
* @param mcurrentType
*/
public void setType(Type mcurrentType){
this.mcurrentType=mcurrentType;
}
}複製程式碼
檢視更多精彩請檢視個人部落格
最後為大家獻上自定義view————loading框 github demo,後期會將loading框優化,新增各種樣式