EmptyLayout:界面多状态加载

427 阅读3分钟

需求

在项目开发的过程中,需要对Api的数据的不同情况反映到UI界面上,以达到良好的用户体验,Api的数据的状态大致可分为异步请求数据、正常返回数据、空数据、加载失败、网络错误等一些状态,相应的布局状态也可分为加载中、正常显示、无数据、加载出错等状态,于是就有了对多状态布局进行管理的需求。

在早期这个需求刚被提出来的时候,最开始的做法是使用FrameLayout或LinearLayout布局里面嵌套对应状态的xml布局,在各种状态布局切换时,需要在Activity或Fragment里使用java代码频繁的调用各种view的显示与隐藏(小白的做法),最后这样做出来造成代码臃肿,操作繁琐,而且不可复用,最后不得不重构,于是就有了EmptyLayout多状态布局。

父容器的选择

基于布局特点、扩展性、性能方面考虑,RelativeLayout由于在 layout 时需要 measure 两次被忽略,LinearLayout也可以作为父布局,不过FrameLayout的层次的特性更适合,比如在空布局状态时需要在顶部显示另外的view的情况下,如下图所示

empty

定义过程

首先为各种布局状态定义标志位

private static final int STATUS_NORMAL = 1;
private static final int STATUS_LOADING = 2;
private static final int STATUS_EMPTY = 3;
private static final int STATUS_ERROR = 4;
//当前的状态,默认是normal
private int mLayoutState=STATUS_NORMAL;

接着在attrs.xml中定义自定义属性,用于配置在不同状态页的默认属性

<!-- EmptyLayout start -->
<declare-styleable name="FrameEmptyLayout">
    <attr name="error_image" format="reference"/>
    <attr name="error_text" format="string"/>
    <attr name="error_retry_text" format="string"/>
    
    <attr name="empty_image" format="reference"/>
    <attr name="empty_text" format="string"/>
</declare-styleable>
<!-- EmptyLayout end -->

然后在初始化的时候通过TypedArray进行赋值
类内部维护一个View的List 的集合,为非正常布局各定义一个Tag

private static final String TAG_LOADING = "FrameEmptyLayout.TAG_LOADING";
private static final String TAG_EMPTY = "FrameEmptyLayout.TAG_EMPTY";
private static final String TAG_ERROR = "FrameEmptyLayout.TAG_ERROR";

重写FrameLayout的 addView方法把非正常布局过滤

@Override
public void addView(@NonNull View child, int index, ViewGroup.LayoutParams params) {
    super.addView(child, index, params);
    if (child.getTag() == null || (!child.getTag().equals(TAG_EMPTY) && !child.getTag().equals(TAG_ERROR)&&!child.getTag().equals(TAG_LOADING))){
        contentViews.add(child);
    }
}

然后在切换布局时把Tag设置到对应的View,比如setErrorView

if (errorView == null) {
    errorView = inflater.inflate(R.layout.layout_error, this, false);
    errorView.setTag(TAG_ERROR);

    errorImageView = errorView.findViewById(R.id.iv_error_icon);
    errorTextView = errorView.findViewById(R.id.tv_error_text);
    errorRetry = errorView.findViewById(R.id.tv_error_retry);

    layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    layoutParams.gravity = Gravity.CENTER;

    addView(errorView, layoutParams);
} else {
    errorView.setVisibility(VISIBLE);
}

最后在对外开放一个接口,提供错误重试的回调

public void setRetryListener(OnRetryClickListener retryListener) {
    this.retryListener = retryListener;
}

public interface OnRetryClickListener {
    void onClick();
}

这样一来就把各个布局状态的切换操作封装到自定义组合的Layout中了,只需要对外开放api就行了

// 加载中
emptyLayout.showLoading();
//正常状态
emptyLayout.showContent();
// 空数据
emptyLayout.showEmpty(R.drawable.ic_launcher_foreground,"暂无数据!",skipId);
//带有重试按钮的错误页
emptyLayout.showError(R.drawable.net_error,"不知道什么原因加载出错了!","点击重试");
//不带重试按钮只显示错误页
emptyLayout.showError(R.drawable.net_error,"不知道什么原因加载出错了!",null);

其中的skipId是上图中需要在空数据状态下显示的view的Id的集合。基于LinearLayout的使用场景比较多,我还实现了LinearEmptyLayout简版多状态切换的Layout,详细的代码以及Sample可以去我的github查看,觉得还可以的不妨Star或follow

思考

EmptyLayout虽然已经有了很多的应用场景,但是如果你的页面是基于RecyclerView写的,本着避免view嵌套的原则,利用修饰者模式,可以为RecyclerView定义一个EmptyWapper的Adapter,通过Adapter的type控制空页面和错误页面切换。