在Android开发过程中通常在有网络请求的页面,需要设计加载中、加载失败等UI效果,来提升用户体验。本文就此需求实现了一个简单的LoadingLayout控件,可以比较方便的实现加载中、加载失败、网络错误等UI效果,并提供失败点击重试等操作。
常用一般有以下几种请求状态:
除加载成功状态每种状态都对应着一种UI效果对应各自View,不同状态的View的展示和消失肯定是通过在ViewGroup添加和删除View实现的,所以我们的LoadingLayout需要继承一个ViewGroup,此ViewGroup我们选择为FrameLayout,不同状态的View可以通过自定义属性自由指定,然后通过开放接口实现切换UI的效果就可以了。
<resources>
<item name="loading_layout_id" type="id" />
<declare-styleable name="LoadingLayout">
<attr name="loadingViewDrawable" format="reference"/>
<attr name="loadFailureViewDrawable" format="reference"/>
<attr name="netErrorViewDrawable" format="reference"/>
<attr name="nullDataViewDrawable" format="reference"/>
declare-styleable>
resources>
定义了一个默认ID和4中UI状态的属性。
public class LoadingLayout extends FrameLayout {
private static final String TAG = LoadingLayout.class.getSimpleName();
/** 加载中 */
public static final int LOADING_STATE = 0x001;
/** 加载成功 */
public static final int LOAD_SUCCESS_STATE = 0x002;
/** 网络错误 */
public static final int NET_ERROR_STATE = 0x003;
/** 加载失败,超时、接口错误、404等 */
public static final int LOAD_FAILURE_STATE = 0x004;
/** 没有数据 */
public static final int NULL_DATA_STATE = 0x005;
private View loadingView; //loading 加载中。。
private View netErrorView; //网络错误View
private View loadFailureView; //加载失败View
private View nullDataView; //没有数据View
//定义注释限定参数值,可以去掉
@IntDef({LOADING_STATE, LOAD_SUCCESS_STATE, NET_ERROR_STATE, LOAD_FAILURE_STATE,NULL_DATA_STATE})
public @interface LoadingState{}
private final LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
public LoadingLayout(@NonNull Context context) {
this(context, null);
}
public LoadingLayout(@NonNull Context context, @NonNull AttributeSet attrs) {
this(context, attrs, 0);
}
public LoadingLayout(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//指定默认ID
if(getId() == -1){
setId(R.id.loading_layout_id);
}
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.LoadingLayout, 0, 0);
try{
//解析自定义属性,为每种状态指定默认效果
int progressViewRes = a.getResourceId(R.styleable.LoadingLayout_loadingViewDrawable,
R.layout.default_loading_layout);
int netErrorViewRes = a.getResourceId(R.styleable.LoadingLayout_netErrorViewDrawable,
R.layout.default_net_error_layout);
int loadFailureViewRes = a.getResourceId(R.styleable.LoadingLayout_loadFailureViewDrawable,
R.layout.default_load_failure_layout);
int nullDataViewRes = a.getResourceId(R.styleable.LoadingLayout_nullDataViewDrawable,
R.layout.default_null_data_layout);
loadingView = LayoutInflater.from(context).inflate(progressViewRes, null, true);
netErrorView = LayoutInflater.from(context).inflate(netErrorViewRes, null, true);
loadFailureView = LayoutInflater.from(context).inflate(loadFailureViewRes, null, true);
nullDataView = LayoutInflater.from(context).inflate(nullDataViewRes, null, true);
} catch (Exception e){
e.printStackTrace();
}
a.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
this.layoutParams.width = this.getMeasuredWidth();
this.layoutParams.height = this.getMeasuredHeight();
}
/**
* 通过状态刷新界面显示
* @param state 当前状态
*/
public void refreshView(@LoadingState int state){
refreshView(state, null);
}
/**
* 通过状态刷新界面显示
* @param state 当前状态
* @param onClickListener 点击监听
*/
public void refreshView(@LoadingState int state, OnClickListener onClickListener){
switch (state){
case LOAD_SUCCESS_STATE:
//加载成功,移除所有状态View
removeStateView();
break;
case LOADING_STATE:
showLoading();
break;
case NET_ERROR_STATE:
showNetErrorMessage(onClickListener);
break;
case LOAD_FAILURE_STATE:
showLoadFailureMessage(onClickListener);
break;
case NULL_DATA_STATE:
showNullDataMessage();
break;
default:
Log.d(TAG, "state error");
break;
}
}
/**
* 显示loading
*/
public void showLoading(){
removeStateView();
if(loadingView != null){
this.addView(loadingView);
} else{
Log.d(TAG, "Please init loadingView first");
}
}
/**
* 显示网络错误提示
*/
public void showNetErrorMessage(){
showNetErrorMessage(null);
}
/**
* 显示网络错误提示
* @param onClickListener 指定点击效果监听
*/
public void showNetErrorMessage(OnClickListener onClickListener){
removeStateView();
if(netErrorView != null){
if(onClickListener != null){
netErrorView.setOnClickListener(onClickListener);
}
this.addView(netErrorView);
} else{
Log.d(TAG, "Please init netErrorView first");
}
}
/**
* 显示无数据提示
*/
public void showNullDataMessage(){
removeStateView();
if(nullDataView != null){
this.addView(nullDataView);
} else{
Log.d(TAG, "Please init nullDataView first");
}
}
/**
* 显示加载失败提示
*/
public void showLoadFailureMessage(){
showLoadFailureMessage(null);
}
/**
* 显示加载失败提示
* @param onClickListener 指定点击效果监听
*/
public void showLoadFailureMessage(OnClickListener onClickListener){
removeStateView();
if(loadFailureView != null){
if(onClickListener != null){
loadFailureView.setOnClickListener(onClickListener);
}
this.addView(loadFailureView);
} else{
Log.d(TAG, "Please init loadFailureView first");
}
}
/**
* 移除所有状态View
*/
private void removeStateView(){
if(loadingView != null){
this.removeView(loadingView);
}
if(netErrorView != null){
this.removeView(netErrorView);
}
if(loadFailureView != null){
this.removeView(loadFailureView);
}
if(nullDataView != null){
this.removeView(nullDataView);
}
}
}
代码注释比较详细了,不过多解释。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<ProgressBar
android:layout_width="50dp"
android:layout_height="50dp"
style="@android:style/Widget.ProgressBar.Large"
android:layout_centerInParent="true"
android:indeterminateDrawable="@drawable/loading_anim"/>
RelativeLayout>
loading_anim:
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/loading"
android:duration="100"
android:fromDegrees="0"
android:interpolator="@android:anim/linear_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="720" />
其他的样式比较简单,就不一一列举了。
布局:
<com.zhong.loadingview.view.LoadingLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toTopOf="@+id/net_error_btn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<ImageView
android:id="@+id/image_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center" />
com.zhong.loadingview.view.LoadingLayout>
需要在父布局中引用自定义属性LoadingLayout。
代码实现:
在代码中需要通过loadingLayout.showLoading();
,loadingLayout.refreshView(state);
等方法来控制状态展示效果。
在写完这个控件之后发现网上有好多类似的实现,好吧。。。至少说明我的实现思路应该是没问题的。但是这种实现其实并不完美: