一、 效果图
二、需求
- 请求网络数据时为了对用户进行友好提示,显示一个Loading框
- 多个网络请求可以共用加载框,直到所有请求结束,Loading框消失
- 不影响下个界面的请求Loading显示与消失
三、需求分析(推荐自定义View)
- 可以通过Dialog或者addView添加到DecorView的方式显示加载框
- 可以通过AnimationDrawable或者自定View的重绘实现加载的动态效果
- 推荐使用自定View+addView的方法实现,因为内存占用小,下面是Dialog+Drawable、自定义View+Dialog、自定义View +addView的对比
- 软引用+count计数实现多个网络请求,
- 判断当前Activity区别页面的显示加载框,避免加载框显示混乱
四、主要代码
1 Drawable的方式
load_view_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/load_icon1" android:duration="100"></item>
<item android:drawable="@drawable/load_icon2" android:duration="100"></item>
<item android:drawable="@drawable/load_icon3" android:duration="100"></item>
<item android:drawable="@drawable/load_icon4" android:duration="100"></item>
<item android:drawable="@drawable/load_icon5" android:duration="100"></item>
<item android:drawable="@drawable/load_icon6" android:duration="100"></item>
<item android:drawable="@drawable/load_icon7" android:duration="100"></item>
<item android:drawable="@drawable/load_icon8" android:duration="100"></item>
<item android:drawable="@drawable/load_icon9" android:duration="100"></item>
<item android:drawable="@drawable/load_icon10" android:duration="100"></item>
</animation-list>
LoadDialog
public class LoadDialog extends Dialog implements OnClickListener {
private ImageView iv_load;
private TextView tips_loading_msg, tv_cancel;
private AnimationDrawable animationDrawable;
private String msg = "正在加载...";
private boolean flag = true;
private Context context;
boolean cancelShow = true;
private int referenceTime = 0;
public LoadDialog(Context context) {
super(context);
this.context = context;
}
public Context getBaseContext() {
return context;
}
public LoadDialog(Context context, String message, boolean flag, boolean cancelShow) {
super(context, R.style.load_dialog);
this.setCancelable(flag);
this.setCanceledOnTouchOutside(false);
this.cancelShow = cancelShow;
if (null == message) {
msg = "正在加载...";
} else {
this.msg = message;
}
referenceTime = 0;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View view = View.inflate(getContext(), R.layout.load_view, null);
this.setContentView(view);
tips_loading_msg = findViewById(R.id.tips_loading_msg);
iv_load = findViewById(R.id.iv_load);
iv_load.setImageResource(R.drawable.load_view_animation);
animationDrawable = (AnimationDrawable) iv_load.getDrawable();
tv_cancel = findViewById(R.id.tv_cancel);
tv_cancel.setClickable(true);
getWindow().getDecorView().setPadding(0, 0, 0, 0);
if (cancelShow) {
tv_cancel.setVisibility(View.VISIBLE);
} else {
tv_cancel.setVisibility(View.GONE);
}
}
@Override
protected void onStart() {
super.onStart();
this.setCancelable(flag);
if (null != msg) {
tips_loading_msg.setText(msg);
}
if (cancelShow) {
tv_cancel.setVisibility(View.VISIBLE);
} else {
tv_cancel.setVisibility(View.GONE);
}
tv_cancel.setOnClickListener(this);
}
public static WeakReference<LoadDialog> dialogReference;
/**
* 只有最新的dialog会显示,后面的会覆盖前面的,自动释放
* runOnUiThread中使用
*/
public static void showDialog(Context context) {
try {
LoadDialog loadDialog = getCorrectDialog(context, false);
loadDialog.referenceTime++;
if (!loadDialog.isShowing()) {
loadDialog.show();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static synchronized LoadDialog getCorrectDialog(Context context, boolean cancelShow) {
LoadDialog loadDialog;
if (null == dialogReference) {
loadDialog = new LoadDialog(context, null, cancelShow, true);
dialogReference = new WeakReference<>(loadDialog);
} else {
//是不是当前Activity或者Fragment的Dialog
loadDialog = dialogReference.get();
if (loadDialog == null) {
loadDialog = new LoadDialog(context, null, cancelShow, true);
dialogReference = new WeakReference<>(loadDialog);
} else if (loadDialog.getBaseContext() != context) {
//是其他Activity或Fragment的Dialog则释放,然后重新创建
if (loadDialog.isShowing()) {
loadDialog.dismiss();
}
loadDialog.release();
loadDialog = new LoadDialog(context, null, cancelShow, true);
dialogReference = new WeakReference<>(loadDialog);
}
}
return loadDialog;
}
public static synchronized void showDialog(Context context, boolean cancelShow) {
try {
LoadDialog loadDialog = getCorrectDialog(context, cancelShow);
loadDialog.referenceTime++;
if (!loadDialog.isShowing()) {
loadDialog.show();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 隐藏最新的dialog
* runOnUiThread中使用
*/
public static synchronized void dismissDialog() {
try {
if (dialogReference == null || dialogReference.get() == null) {
return;
}
LoadDialog loadDialog = dialogReference.get();
if (loadDialog.referenceTime > 0) {
loadDialog.referenceTime--;
}
if (loadDialog.referenceTime == 0) {
if (loadDialog.isShowing()) {
loadDialog.dismiss();
}
loadDialog.release();
dialogReference.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 清楚当前显示的Dialog
*/
public static synchronized void clear() {
if(dialogReference == null || dialogReference.get() == null){
return;
}
LoadDialog loadDialog = dialogReference.get();
if(loadDialog.isShowing()){
loadDialog.dismiss();
}
loadDialog.referenceTime = 0;
loadDialog.release();
dialogReference.clear();
}
/**
* 清除当前Activity或者Fragment对应Dialog
* @param context
*/
public static synchronized void clear(Context context) {
if(dialogReference == null || dialogReference.get() == null){
return;
}
LoadDialog loadDialog = dialogReference.get();
if (loadDialog.getBaseContext() == context) {
if (loadDialog.isShowing()) {
loadDialog.dismiss();
}
loadDialog.release();
dialogReference.clear();
loadDialog.referenceTime = 0;
}
}
private void release() {
if (animationDrawable == null) {
release();
for (int i = 0; i < animationDrawable.getNumberOfFrames(); i++) {
Drawable frame = animationDrawable.getFrame(i);
if (frame instanceof BitmapDrawable) {
((BitmapDrawable) frame).getBitmap().recycle();
}
frame.setCallback(null);
}
animationDrawable.setCallback(null);
}
}
@Override
public void show() {
if (null == msg) {
msg = "正在加载...";
}
super.show();
animationDrawable.start();
}
@Override
public void dismiss() {
super.dismiss();
referenceTime = 0;
animationDrawable.stop();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tv_cancel:
dismiss();
break;
}
}
}
2 自定义View的实现(推荐)
LoadingView
public class LoadingView extends View {
private Paint paint = new Paint();
private Paint mBitMapPaint;
private Bitmap mBitMapSRC, mBitMapDST, mBgBitmap;
private int dx;
private ValueAnimator animator;
public LoadingView(Context context) {
this(context, null);
}
public LoadingView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
mBitMapPaint = new Paint();
mBitMapPaint.setColor(Color.RED);
mBgBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.load1);
mBitMapDST = BitmapFactory.decodeResource(getResources(), R.drawable.load2);
mBitMapSRC = Bitmap.createBitmap(mBitMapDST.getWidth(), mBitMapDST.getHeight(), Bitmap.Config.ARGB_8888);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w, h;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
w = measureWidth;
h = measureHeight;
mBgBitmap = BitmapUtil.scaleBitmap(mBgBitmap, w, h);
} else if (widthMode == MeasureSpec.EXACTLY) {
w = measureWidth;
h = (measureWidth * mBgBitmap.getHeight()) / mBgBitmap.getWidth();
mBgBitmap = BitmapUtil.scaleBitmap(mBgBitmap, w, h);
} else if (heightMode == MeasureSpec.EXACTLY) {
h = measureHeight;
w = (measureHeight * mBgBitmap.getWidth()) / mBgBitmap.getHeight();
mBgBitmap = BitmapUtil.scaleBitmap(mBgBitmap, w, h);
} else {
w = mBgBitmap.getWidth();
h = mBgBitmap.getHeight();
}
mBitMapDST = BitmapUtil.scaleBitmap(mBitMapDST, w, h);
mBitMapSRC = BitmapUtil.scaleBitmap(mBitMapSRC, w, h);
setMeasuredDimension(w, h);
setBackgroundDrawable(new BitmapDrawable(getResources(), mBgBitmap));
startAnimation();
}
Canvas c;
@Override
protected void onDraw(Canvas canvas) {
// canvas.drawBitmap(mBgBitmap, 0, 0, paint);
int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
if (c == null) {
c = new Canvas(mBitMapSRC);
}
c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// 画不透明的矩形区域
c.drawRect(dx, 0, mBitMapDST.getWidth(), mBitMapDST.getHeight(), mBitMapPaint);
// 画目标图片
canvas.drawBitmap(mBitMapDST, 0, 0, mBitMapPaint);
mBitMapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(mBitMapSRC, 0, 0, mBitMapPaint);
mBitMapPaint.setXfermode(null);
canvas.restoreToCount(layerId);
}
public synchronized void startAnimation() {
if (animator == null) {
initAnimation();
}
if (animator.isRunning()) {
return;
}
animator.start();
}
public synchronized void stopAnimation() {
animator.cancel();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
recycleBitmap(mBgBitmap);
recycleBitmap(mBitMapDST);
recycleBitmap(mBitMapSRC);
if(animator != null && animator.isRunning()){
animator.cancel();
}
}
private void recycleBitmap(Bitmap bitmap) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
private void initAnimation() {
animator = ValueAnimator.ofInt(0, mBitMapDST.getWidth());
animator.setDuration(1000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator() {
@Override
public float getInterpolation(float input) {
return 1.2f * input;
}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
dx = (int) animation.getAnimatedValue();
Log.i("LoadingViewDialog","animation dx="+dx );
postInvalidate();
}
});
}
}
SnackLoading
public class SnackLoading {
private static WeakReference<SnackLoading> snackInstance;
private Activity activity;
private View mView;
private LoadingView loadingView;
private int referenceTime;
private SnackLoading(Activity activity) {
this.activity = activity;
mView = View.inflate(activity, R.layout.snack_load_view, null);
((ViewGroup) activity.getWindow().getDecorView()).addView(mView);
loadingView = mView.findViewById(R.id.iv_load);
mView.findViewById(R.id.tv_cancel).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
referenceTime =0;
dismiss();
}
});
referenceTime = 0;
}
public void show() {
if (snackInstance == null || snackInstance.get() == null) {
return;
}
SnackLoading snackLoading = snackInstance.get();
snackLoading.referenceTime++;
if (mView != null && mView.getVisibility() == View.GONE) {
mView.setVisibility(View.VISIBLE);
}
if (loadingView != null)
loadingView.startAnimation();
}
public void dismiss() {
if (snackInstance == null || snackInstance.get() == null) {
return;
}
SnackLoading loadDialog = snackInstance.get();
if (loadDialog.referenceTime > 0) {
loadDialog.referenceTime--;
}
if (loadDialog.referenceTime == 0) {
if (mView != null) {
mView.setVisibility(View.GONE);
}
if (loadingView != null) {
loadingView.stopAnimation();
}
}
}
public void clear() {
if (loadingView != null) {
loadingView.stopAnimation();
}
if (mView != null && mView.getParent() != null) {
((ViewGroup) mView.getParent()).removeView(mView);
}
}
public static synchronized SnackLoading getInstance(Activity activity) {
SnackLoading snackLoading;
if (snackInstance == null || snackInstance.get() == null) {
snackLoading = new SnackLoading(activity);
snackInstance = new WeakReference<>(snackLoading);
} else {
snackLoading = snackInstance.get();
if (snackLoading.activity != activity) {
snackInstance.clear();
snackLoading = new SnackLoading(activity);
snackInstance = new WeakReference<>(snackLoading);
}
}
return snackLoading;
}
}
五 Demo
六 注意
- Bitmap、Drawable、Animator要回收
- 自定义View中onDraw的Canvas要声明成全局,否则内存开销大
七 LoadingView进一步优化
- 绘制Loading效果,可以直接用Bitmap,不需要saveLayer
- ValueAnimator泄漏:通过添加Activity的监听器,及时cancel掉Animator
修改后的LoadingView(注意Fragment不用v4的,兼容)
/**
* Created by Ray on 2019-12-18.
*/
public class LoadingView extends View {
private final String TAG = "LoadingView";
private Paint mBitMapPaint;
private Bitmap mBitMapDST;
private Rect rect;
private int dx;
private ValueAnimator animator;
private LifeListener mLifeListener = new LifeListener() {
@Override
public void onCreate(Bundle bundle) {
}
@Override
public void onStart() {
Log.d(TAG, "onStart() ");
}
@Override
public void onResume() {
Log.d(TAG, "onResume() ");
}
@Override
public void onPause() {
Log.d(TAG, "onPause() ");
}
@Override
public void onStop() {
Log.d(TAG, "onStop() ");
}
@Override
public void onDestroy() {
if (animator != null ) {
animator.cancel();
animator = null;
}
Log.d(TAG, "onDestroy() ");
}
};
public LoadingView(Context context) {
this(context, null);
}
public LoadingView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
mBitMapPaint = new Paint();
setBackgroundResource(R.drawable.load1);
mBitMapDST = BitmapFactory.decodeResource(getResources(), R.drawable.load2);
rect = new Rect();
rect.top = 0;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w, h;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
w = measureWidth;
h = measureHeight;
} else if (widthMode == MeasureSpec.EXACTLY) {
w = measureWidth;
h = (measureWidth * mBitMapDST.getHeight()) / mBitMapDST.getWidth();
} else if (heightMode == MeasureSpec.EXACTLY) {
h = measureHeight;
w = (measureHeight * mBitMapDST.getWidth()) / mBitMapDST.getHeight();
} else {
w = mBitMapDST.getWidth();
h = mBitMapDST.getHeight();
}
mBitMapDST = scaleBitmap(mBitMapDST, w, h);
setMeasuredDimension(w, h);
startAnimation();
}
@Override
protected void onDraw(Canvas canvas) {
rect.bottom = getHeight();
rect.right = getWidth();
rect.left = dx;
canvas.drawBitmap(mBitMapDST, rect, rect, mBitMapPaint);
}
public synchronized void startAnimation() {
if (animator == null) {
initAnimation();
}
if (animator.isRunning()) {
return;
}
post(() -> animator.start());
}
public static Bitmap scaleBitmap(Bitmap origin, int newWidth, int newHeight) {
if (origin == null) {
return null;
}
int height = origin.getHeight();
int width = origin.getWidth();
if (newWidth == width && newHeight == height) {
return origin;
}
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);// 使用后乘
Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
if (!origin.isRecycled()) {
origin.recycle();
}
return newBM;
}
public synchronized void stopAnimation() {
if (animator != null && animator.isRunning()) {
animator.cancel();
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Activity activity = getActivity();
if (activity != null) {
addLifeListener(activity);
}
}
//获取宿主Activity
private Activity getActivity() {
View parent = this;
Activity activity = null;
do {
final Context context = parent.getContext();
// Log.d(TAG, "view: " + parent + ", context: " + context);
if (context != null && context instanceof Activity) {
activity = (Activity) context;
break;
}
} while ((parent = (View) parent.getParent()) != null);
return activity;
}
private void addLifeListener(Activity activity) {
LifeListenerFragment fragment = getLifeListenerFragment(activity);
fragment.addLifeListener(mLifeListener);
}
private LifeListenerFragment getLifeListenerFragment(Activity activity) {
FragmentManager manager = activity.getFragmentManager();
return getLifeListenerFragment(manager);
}
//添加空白fragment
private LifeListenerFragment getLifeListenerFragment(FragmentManager manager) {
LifeListenerFragment fragment = (LifeListenerFragment) manager.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new LifeListenerFragment();
manager.beginTransaction().add(fragment, TAG).commitAllowingStateLoss();
}
return fragment;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
recycleBitmap(mBitMapDST);
if (animator != null && animator.isRunning()) {
animator.cancel();
}
}
private void recycleBitmap(Bitmap bitmap) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
private void initAnimation() {
animator = ValueAnimator.ofInt(0, mBitMapDST.getWidth());
animator.setDuration(1000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator() {
@Override
public float getInterpolation(float input) {
return 1.2f * input;
}
});
animator.addUpdateListener(animation -> {
if (Math.abs((int) animation.getAnimatedValue() - dx) > 5) {
dx = (int) animation.getAnimatedValue();
postInvalidate();
}
});
}
//生命周期回调接口
public interface LifeListener {
void onCreate(Bundle bundle);
void onStart();
void onResume();
void onPause();
void onStop();
void onDestroy();
}
//空白Fragment
public static class LifeListenerFragment extends Fragment {
private LifeListener mLifeListener;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public void addLifeListener(LifeListener listener) {
mLifeListener = listener;
}
public void removeLifeListener() {
mLifeListener = null;
}
@Override
public void onStart() {
super.onStart();
if (mLifeListener != null) {
mLifeListener.onStart();
}
}
@Override
public void onStop() {
super.onStop();
if (mLifeListener != null) {
mLifeListener.onStop();
}
}
@Override
public void onResume() {
super.onResume();
if (mLifeListener != null) {
mLifeListener.onResume();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mLifeListener != null) {
mLifeListener.onDestroy();
}
removeLifeListener();
}
}
}