Android 状态管理优化

01.界面状态有哪些

  • 在Android中,不管是activity或者fragment,在加载视图的时候都有可能会出现多种不同的状态页面View。比如常见的就有这些:
    • 内容界面,也就是正常有数据页面
    • 加载数据中,加载loading
    • 加载数据错误,请求数据异常
    • 加载后没有数据,请求数据为空
    • 没有网络,网络异常
  • 同时,思考一下几个问题。
    • 怎样切换界面状态?有些界面想定制自定义状态?状态如何添加点击事件?下面就为解决这些问题!
  • 为何要这样?
    • 一般在加载网络数据时,需要用户等待的场景,显示一个加载的Loading动画可以让用户知道App正在加载数据,而不是程序卡死,从而给用户较好的使用体验。
    • 当加载的数据为空时显示一个数据为空的视图、在数据加载失败时显示加载失败对应的UI并支持点击重试会比白屏的用户体验更好一些。
    • 加载中、加载失败、空数据等不同状态页面风格,一般来说在App内的所有页面中需要保持一致,也就是需要做到全局统一。

02.采用include方式管理

  • 直接把这些界面include到main界面中,然后动态去切换界面,具体一点的做法如下所示。

    • 在布局中,会存放多个状态的布局。然后在页面中根据逻辑将对应的布局给显示或者隐藏,但存在诸多问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!--正常时布局-->
    <include layout="@layout/activity_content"/>
    <!--加载loading布局-->
    <include layout="@layout/activity_loading"/>
    <!--异常时布局-->
    <include layout="@layout/activity_error"/>
    <!--空数据时布局-->
    <include layout="@layout/activity_emptydata"/>

    </LinearLayout>
  • 存在的问题分析

    • 后来发现这样处理不容易复用到其他项目中,代码复用性很低
    • 在activity中处理这些状态的显示和隐藏比较乱
    • 调用setContentView方法时,是将所有的布局给加载绘制出来。其实没有必要
    • 如果将逻辑写在BaseActivity中,利用子类继承父类特性,在父类中写切换状态,但有些界面如果没有继承父类,又该如何处理

03.在Base类中处理逻辑

  • 首先是定义一个自定义的控件,比如把它命名成LoadingView,然后在这个里面include一个布局,该布局包含一些不同状态的视图。代码思路如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    public class LoadingView extends LinearLayout implements View.OnClickListener {

    public static final int LOADING = 0;
    public static final int STOP_LOADING = 1;
    public static final int NO_DATA = 2;
    public static final int NO_NETWORK = 3;
    public static final int GONE = 4;
    public static final int LOADING_DIALOG = 5;

    private TextView mNoDataTextView;
    private ProgressBar mLoadingProgressBar;
    private RelativeLayout mRlError;
    private LinearLayout mLlLoading;
    private View mView;

    private OnRefreshListener mListener;

    public void setRefrechListener(OnRefreshListener mListener) {
    this.mListener = mListener;
    }

    public interface OnRefreshListener {
    void refresh();
    }

    public LoadingView(Context context) {
    super(context);
    init(context);
    }

    public LoadingView(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
    }

    public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
    }

    private void init(Context context) {
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    mView = inflater.inflate(R.layout.common_loading_get, this);
    mLoadingProgressBar = (ProgressBar) mView.findViewById(R.id.mLoadingProgressBar);
    mNoDataTextView = (TextView) mView.findViewById(R.id.mNoDataTextView);
    mLlLoading = (LinearLayout) mView.findViewById(R.id.ll_loading);
    mRlError = (RelativeLayout) mView.findViewById(R.id.rl_error);
    mRlError.setOnClickListener(this);
    setStatue(GONE);
    }

    public void setStatue(int status) {
    setVisibility(View.VISIBLE);
    try {
    if (status == LOADING) {//更新
    mRlError.setVisibility(View.GONE);
    mLlLoading.setVisibility(View.VISIBLE);
    } else if (status == STOP_LOADING) {
    setVisibility(View.GONE);
    } else if (status == NO_DATA) {//无数据情况
    mRlError.setVisibility(View.VISIBLE);
    mLlLoading.setVisibility(View.GONE);
    mNoDataTextView.setText("暂无数据");
    } else if (status == NO_NETWORK) {//无网络情况
    mRlError.setVisibility(View.VISIBLE);
    mLlLoading.setVisibility(View.GONE);
    mNoDataTextView.setText("网络加载失败,点击重新加载");
    } else {
    setVisibility(View.GONE);
    }
    } catch (OutOfMemoryError e) {
    }
    }

    @Override
    public void onClick(View v) {
    mListener.refresh();
    setStatue(LOADING);
    }
    }
  • 然后在BaseActivity/BaseFragment中封装LoadingView的初始化逻辑,并封装加载状态切换时的UI显示逻辑,暴露给子类以下方法:

    1
    2
    3
    4
    void showLoading(); //调用此方法显示加载中的动画
    void showLoadFailed(); //调用此方法显示加载失败界面
    void showEmpty(); //调用此方法显示空页面
    void onClickRetry(); //子类中实现,点击重试的回调方法
    • 在BaseActivity/BaseFragment的子类中可通过上一步的封装比较方便地使用加载状态显示功能。这种使用方式耦合度太高,每个页面的布局文件中都需要添加LoadingView,使用起来不方便而且维护成本较高,比如说有时候异常状态的布局各个页面不同,那么难以自定义处理,修改起来成本较高。
    • 同时如果是要用这种状态管理工具,则需要在需要的页面布局中添加该LoadingView视图。这样也能够完成需求,但是感觉有点麻烦。
  • 具体如何使用它进行状态管理呢?可以看到在对应的布局中需要写上LoadingView

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.cheoo.app.view.recyclerview.TypeRecyclerView
    android:id="@+id/mRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:overScrollMode="never"
    android:scrollbars="none">

    </com.cheoo.app.view.recyclerview.TypeRecyclerView>

    <com.cheoo.app.view.LoadingView
    android:id="@+id/mLoadingView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    </RelativeLayout>
  • 那么,如果某个子类不想继承BaseActivity类,如何使用该状态管理器呢?代码中可以这种使用。

    1
    2
    3
    4
    5
    mLoadingView  = (LoadingView)findViewById(R.id.mLoadingView);
    mLoadingView.setStatue(LoadingView.LOADING);
    mLoadingView.setStatue(LoadingView.STOP_LOADING);
    mLoadingView.setStatue(LoadingView.NO_NETWORK);
    mLoadingView.setStatue(LoadingView.NO_DATA);

04.如何降低偶性和入侵性

  • 让View状态的切换和Activity彻底分离开,必须把这些状态View都封装到一个管理类中,然后暴露出几个方法来实现View之间的切换。
    在不同的项目中可以需要的View也不一样,所以考虑把管理类设计成builder模式来自由的添加需要的状态View。
  • 那么如何降低耦合性,让代码入侵性低。方便维护和修改,且移植性强呢?大概具备这样的条件……
    • 可以运用在activity或者fragment中
    • 不需要在布局中添加LoadingView,而是统一管理不同状态视图,同时暴露对外设置自定义状态视图方法,方便UI特定页面定制
    • 支持设置自定义不同状态视图,即使在BaseActivity统一处理状态视图管理,也支持单个页面定制
    • 在加载视图的时候像异常和空页面能否用ViewStub代替,这样减少绘制,只有等到出现异常和空页面时,才将视图给inflate出来
    • 当页面出现网络异常页面,空页面等,页面会有交互事件,这时候可以设置点击设置网络或者点击重新加载等等

05.封装低入侵性状态库

5.1 自定义帧布局

  • 首先需要自定义一个状态StateFrameLayout布局,它是继承FrameLayout。在这个类中,目前是设置五种不同状态的视图布局,主要的功能操作是显示或者隐藏布局。为了后期代码维护性,根据面向对象的思想,类尽量保证单一职责,所以关于状态切换,以及设置自定义状态布局,把这个功能分离处理,放到一个StateLayoutManager中处理。

    • 看代码可知,这个类的功能非常明确,就是隐藏或者展示视图作用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    /**
    * 自定义帧布局
    */
    public class StateFrameLayout extends FrameLayout {

    /**
    * loading 加载id
    */
    public static final int LAYOUT_LOADING_ID = 1;

    /**
    * 内容id
    */
    public static final int LAYOUT_CONTENT_ID = 2;

    /**
    * 异常id
    */
    public static final int LAYOUT_ERROR_ID = 3;

    /**
    * 网络异常id
    */
    public static final int LAYOUT_NETWORK_ERROR_ID = 4;

    /**
    * 空数据id
    */
    public static final int LAYOUT_EMPTY_DATA_ID = 5;

    /**
    * 存放布局集合
    */
    private SparseArray<View> layoutSparseArray = new SparseArray<>();

    //private HashMap<Integer,View> map = new HashMap<>();

    /**
    * 布局管理器
    */
    private StateLayoutManager mStatusLayoutManager;


    public StateFrameLayout(Context context) {
    super(context);
    }

    public StateFrameLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    }

    public StateFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    }


    public void setStatusLayoutManager(StateLayoutManager statusLayoutManager) {
    mStatusLayoutManager = statusLayoutManager;
    //添加所有的布局到帧布局
    addAllLayoutToRootLayout();
    }

    private void addAllLayoutToRootLayout() {
    if (mStatusLayoutManager.contentLayoutResId != 0) {
    addLayoutResId(mStatusLayoutManager.contentLayoutResId, StateFrameLayout.LAYOUT_CONTENT_ID);
    }
    if (mStatusLayoutManager.loadingLayoutResId != 0) {
    addLayoutResId(mStatusLayoutManager.loadingLayoutResId, StateFrameLayout.LAYOUT_LOADING_ID);
    }

    if (mStatusLayoutManager.emptyDataVs != null) {
    addView(mStatusLayoutManager.emptyDataVs);
    }
    if (mStatusLayoutManager.errorVs != null) {
    addView(mStatusLayoutManager.errorVs);
    }
    if (mStatusLayoutManager.netWorkErrorVs != null) {
    addView(mStatusLayoutManager.netWorkErrorVs);
    }
    }

    private void addLayoutResId(@LayoutRes int layoutResId, int id) {
    View resView = LayoutInflater.from(mStatusLayoutManager.context).inflate(layoutResId, null);
    layoutSparseArray.put(id, resView);
    addView(resView);
    }

    /**
    * 显示loading
    */
    public void showLoading() {
    if (layoutSparseArray.get(LAYOUT_LOADING_ID) != null) {
    showHideViewById(LAYOUT_LOADING_ID);
    }
    }

    /**
    * 显示内容
    */
    public void showContent() {
    if (layoutSparseArray.get(LAYOUT_CONTENT_ID) != null) {
    showHideViewById(LAYOUT_CONTENT_ID);
    }
    }

    /**
    * 显示空数据
    */
    public void showEmptyData(int iconImage, String textTip) {
    if (inflateLayout(LAYOUT_EMPTY_DATA_ID)) {
    showHideViewById(LAYOUT_EMPTY_DATA_ID);
    emptyDataViewAddData(iconImage, textTip);
    }
    }


    /**
    * 根据ID显示隐藏布局
    * @param id id值
    */
    private void showHideViewById(int id) {
    for (int i = 0; i < layoutSparseArray.size(); i++) {
    int key = layoutSparseArray.keyAt(i);
    View valueView = layoutSparseArray.valueAt(i);
    //显示该view
    if(key == id) {
    valueView.setVisibility(View.VISIBLE);
    if(mStatusLayoutManager.onShowHideViewListener != null) {
    mStatusLayoutManager.onShowHideViewListener.onShowView(valueView, key);
    }
    } else {
    if(valueView.getVisibility() != View.GONE) {
    valueView.setVisibility(View.GONE);
    if(mStatusLayoutManager.onShowHideViewListener != null) {
    mStatusLayoutManager.onShowHideViewListener.onHideView(valueView, key);
    }
    }
    }
    }
    }

    /**
    * 这个是处理ViewStub的逻辑,主要有网络异常布局,加载异常布局,空数据布局
    * @param id 布局id
    * @return 布尔值
    */
    private boolean inflateLayout(int id) {
    boolean isShow = true;
    //如果为null,则直接返回false
    if (layoutSparseArray.get(id) == null) {
    return false;
    }
    switch (id) {
    case LAYOUT_NETWORK_ERROR_ID:
    if (mStatusLayoutManager.netWorkErrorVs != null) {
    View view = mStatusLayoutManager.netWorkErrorVs.inflate();
    retryLoad(view, mStatusLayoutManager.netWorkErrorRetryViewId);
    layoutSparseArray.put(id, view);
    isShow = true;
    } else {
    isShow = false;
    }
    break;
    case LAYOUT_ERROR_ID:
    if (mStatusLayoutManager.errorVs != null) {
    View view = mStatusLayoutManager.errorVs.inflate();
    if (mStatusLayoutManager.errorLayout != null) {
    mStatusLayoutManager.errorLayout.setView(view);
    }
    retryLoad(view, mStatusLayoutManager.errorRetryViewId);
    layoutSparseArray.put(id, view);
    isShow = true;
    } else {
    isShow = false;
    }
    break;
    case LAYOUT_EMPTY_DATA_ID:
    if (mStatusLayoutManager.emptyDataVs != null) {
    View view = mStatusLayoutManager.emptyDataVs.inflate();
    if (mStatusLayoutManager.emptyDataLayout != null) {
    mStatusLayoutManager.emptyDataLayout.setView(view);
    }
    retryLoad(view, mStatusLayoutManager.emptyDataRetryViewId);
    layoutSparseArray.put(id, view);
    isShow = true;
    } else {
    isShow = false;
    }
    break;
    default:
    break;
    }
    return isShow;
    }
    }

5.2 自定义状态管理器

  • 上面状态的自定义布局创建出来了,而且隐藏和展示都做了。那么如何控制设置自定义视图布局,还有如何控制不同布局之间切换,那么就需要用到这个类呢!

    • loadingLayoutResId和contentLayoutResId代表等待加载和显示内容的xml文件
    • 几种异常状态要用ViewStub,因为在界面状态切换中loading和内容View都是一直需要加载显示的,但是其他的3个只有在没数据或者网络异常的情况下才会加载显示,所以用ViewStub来加载他们可以提高性能。
    • 采用builder模式,十分简单,代码如下所示。创建StateFrameLayout对象,然后再设置setStatusLayoutManager,这一步操作是传递一个Manager对象到StateFrameLayout,建立连接。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    public final class StateLayoutManager {

    final Context context;

    final int netWorkErrorRetryViewId;
    final int emptyDataRetryViewId;
    final int errorRetryViewId;
    final int loadingLayoutResId;
    final int contentLayoutResId;
    final int retryViewId;
    final int emptyDataIconImageId;
    final int emptyDataTextTipId;
    final int errorIconImageId;
    final int errorTextTipId;

    final ViewStub emptyDataVs;
    final ViewStub netWorkErrorVs;
    final ViewStub errorVs;
    final AbsViewStubLayout errorLayout;
    final AbsViewStubLayout emptyDataLayout;

    private final StateFrameLayout rootFrameLayout;
    final OnShowHideViewListener onShowHideViewListener;
    final OnRetryListener onRetryListener;

    public static Builder newBuilder(Context context) {
    return new Builder(context);
    }

    private StateLayoutManager(Builder builder) {
    this.context = builder.context;
    this.loadingLayoutResId = builder.loadingLayoutResId;
    this.netWorkErrorVs = builder.netWorkErrorVs;
    this.netWorkErrorRetryViewId = builder.netWorkErrorRetryViewId;
    this.emptyDataVs = builder.emptyDataVs;
    this.emptyDataRetryViewId = builder.emptyDataRetryViewId;
    this.errorVs = builder.errorVs;
    this.errorRetryViewId = builder.errorRetryViewId;
    this.contentLayoutResId = builder.contentLayoutResId;
    this.onShowHideViewListener = builder.onShowHideViewListener;
    this.retryViewId = builder.retryViewId;
    this.onRetryListener = builder.onRetryListener;
    this.emptyDataIconImageId = builder.emptyDataIconImageId;
    this.emptyDataTextTipId = builder.emptyDataTextTipId;
    this.errorIconImageId = builder.errorIconImageId;
    this.errorTextTipId = builder.errorTextTipId;
    this.errorLayout = builder.errorLayout;
    this.emptyDataLayout = builder.emptyDataLayout;

    //创建帧布局
    rootFrameLayout = new StateFrameLayout(this.context);
    ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    rootFrameLayout.setLayoutParams(layoutParams);

    //设置状态管理器
    rootFrameLayout.setStatusLayoutManager(this);
    }


    /**
    * 显示loading
    */
    public void showLoading() {
    rootFrameLayout.showLoading();
    }

    /**
    * 显示内容
    */
    public void showContent() {
    rootFrameLayout.showContent();
    }

    /**
    * 显示空数据
    */
    public void showEmptyData(int iconImage, String textTip) {
    rootFrameLayout.showEmptyData(iconImage, textTip);
    }

    /**
    * 显示空数据
    */
    public void showEmptyData() {
    showEmptyData(0, "");
    }

    /**
    * 显示空数据
    */
    public void showLayoutEmptyData(Object... objects) {
    rootFrameLayout.showLayoutEmptyData(objects);
    }

    /**
    * 显示网络异常
    */
    public void showNetWorkError() {
    rootFrameLayout.showNetWorkError();
    }

    /**
    * 显示异常
    */
    public void showError(int iconImage, String textTip) {
    rootFrameLayout.showError(iconImage, textTip);
    }

    /**
    * 显示异常
    */
    public void showError() {
    showError(0, "");
    }

    public void showLayoutError(Object... objects) {
    rootFrameLayout.showLayoutError(objects);
    }

    /**
    * 得到root 布局
    */
    public View getRootLayout() {
    return rootFrameLayout;
    }

    public static final class Builder {

    private Context context;
    private int loadingLayoutResId;
    private int contentLayoutResId;
    private ViewStub netWorkErrorVs;
    private int netWorkErrorRetryViewId;
    private ViewStub emptyDataVs;
    private int emptyDataRetryViewId;
    private ViewStub errorVs;
    private int errorRetryViewId;
    private int retryViewId;
    private int emptyDataIconImageId;
    private int emptyDataTextTipId;
    private int errorIconImageId;
    private int errorTextTipId;
    private AbsViewStubLayout errorLayout;
    private AbsViewStubLayout emptyDataLayout;
    private OnShowHideViewListener onShowHideViewListener;
    private OnRetryListener onRetryListener;

    Builder(Context context) {
    this.context = context;
    }

    /**
    * 自定义加载布局
    */
    public Builder loadingView(@LayoutRes int loadingLayoutResId) {
    this.loadingLayoutResId = loadingLayoutResId;
    return this;
    }

    /**
    * 自定义网络错误布局
    */
    public Builder netWorkErrorView(@LayoutRes int newWorkErrorId) {
    netWorkErrorVs = new ViewStub(context);
    netWorkErrorVs.setLayoutResource(newWorkErrorId);
    return this;
    }

    /**
    * 自定义加载空数据布局
    */
    public Builder emptyDataView(@LayoutRes int noDataViewId) {
    emptyDataVs = new ViewStub(context);
    emptyDataVs.setLayoutResource(noDataViewId);
    return this;
    }

    /**
    * 自定义加载错误布局
    */
    public Builder errorView(@LayoutRes int errorViewId) {
    errorVs = new ViewStub(context);
    errorVs.setLayoutResource(errorViewId);
    return this;
    }

    /**
    * 自定义加载内容正常布局
    */
    public Builder contentView(@LayoutRes int contentLayoutResId) {
    this.contentLayoutResId = contentLayoutResId;
    return this;
    }

    public Builder errorLayout(AbsViewStubLayout errorLayout) {
    this.errorLayout = errorLayout;
    this.errorVs = errorLayout.getLayoutVs();
    return this;
    }

    public Builder emptyDataLayout(AbsViewStubLayout emptyDataLayout) {
    this.emptyDataLayout = emptyDataLayout;
    this.emptyDataVs = emptyDataLayout.getLayoutVs();
    return this;
    }

    public Builder netWorkErrorRetryViewId(@LayoutRes int netWorkErrorRetryViewId) {
    this.netWorkErrorRetryViewId = netWorkErrorRetryViewId;
    return this;
    }

    public Builder emptyDataRetryViewId(@LayoutRes int emptyDataRetryViewId) {
    this.emptyDataRetryViewId = emptyDataRetryViewId;
    return this;
    }

    public Builder errorRetryViewId(@LayoutRes int errorRetryViewId) {
    this.errorRetryViewId = errorRetryViewId;
    return this;
    }

    public Builder retryViewId(@LayoutRes int retryViewId) {
    this.retryViewId = retryViewId;
    return this;
    }

    public Builder emptyDataIconImageId(@LayoutRes int emptyDataIconImageId) {
    this.emptyDataIconImageId = emptyDataIconImageId;
    return this;
    }

    public Builder emptyDataTextTipId(@LayoutRes int emptyDataTextTipId) {
    this.emptyDataTextTipId = emptyDataTextTipId;
    return this;
    }

    public Builder errorIconImageId(@LayoutRes int errorIconImageId) {
    this.errorIconImageId = errorIconImageId;
    return this;
    }

    public Builder errorTextTipId(@LayoutRes int errorTextTipId) {
    this.errorTextTipId = errorTextTipId;
    return this;
    }

    /**
    * 为状态View显示隐藏监听事件
    * @param listener listener
    * @return
    */
    public Builder onShowHideViewListener(OnShowHideViewListener listener) {
    this.onShowHideViewListener = listener;
    return this;
    }

    /**
    * 为重试加载按钮的监听事件
    * @param onRetryListener listener
    * @return
    */
    public Builder onRetryListener(OnRetryListener onRetryListener) {
    this.onRetryListener = onRetryListener;
    return this;
    }

    /**
    * 创建对象
    * @return
    */
    public StateLayoutManager build() {
    return new StateLayoutManager(this);
    }
    }

    }

5.3 如何管理多种状态

  • 大约5种状态,如何管理这些状态?添加到集合中,Android中选用SparseArray比HashMap更省内存,在某些条件下性能更好,主要是因为它避免了对key的自动装箱(int转为Integer类型),它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**存放布局集合 */
    private SparseArray<View> layoutSparseArray = new SparseArray();

    /**将布局添加到集合 */
    private void addLayoutResId(@LayoutRes int layoutResId, int id) {
        View resView = LayoutInflater.from(mStatusLayoutManager.context).inflate(layoutResId, null);
        layoutSparseArray.put(id, resView);
        addView(resView);
    }

    //那么哪里从集合中取数据呢
    public void showContent() {
    if (layoutSparseArray.get(LAYOUT_CONTENT_ID) != null) {
    showHideViewById(LAYOUT_CONTENT_ID);
    }
    }

06.封装库极致优化点说明

6.1 用ViewStub显示布局

  • 方法里面通过id判断来执行不同的代码,首先判断ViewStub是否为空,如果为空就代表没有添加这个View就返回false,不为空就加载View并且添加到集合当中,然后调用showHideViewById方法显示隐藏View,retryLoad方法是给重试按钮添加事件

    • 注意,即使当你设置了多种不同状态视图,调用setContentView的时候,因为异常页面使用ViewStub,所以在绘制的时候不会影响性能的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    /**
    *  显示loading
    */
    public void showLoading() {
        if (layoutSparseArray.get(LAYOUT_LOADING_ID) != null)
            showHideViewById(LAYOUT_LOADING_ID);
    }

    /**
    *  显示内容
    */
    public void showContent() {
        if (layoutSparseArray.get(LAYOUT_CONTENT_ID) != null)
            showHideViewById(LAYOUT_CONTENT_ID);
    }

    //调用inflateLayout方法,方法返回true然后调用showHideViewById方法
    private boolean inflateLayout(int id) {
        boolean isShow = true;
        if (layoutSparseArray.get(id) != null) return isShow;
        switch (id) {
            case LAYOUT_NETWORK_ERROR_ID:
                if (mStatusLayoutManager.netWorkErrorVs != null) {
                    View view = mStatusLayoutManager.netWorkErrorVs.inflate();
                    retryLoad(view, mStatusLayoutManager.netWorkErrorRetryViewId);
                    layoutSparseArray.put(id, view);
                    isShow = true;
                } else {
                    isShow = false;
                }
                break;
            case LAYOUT_ERROR_ID:
                if (mStatusLayoutManager.errorVs != null) {
                    View view = mStatusLayoutManager.errorVs.inflate();
                    if (mStatusLayoutManager.errorLayout != null) mStatusLayoutManager.errorLayout.setView(view);
                    retryLoad(view, mStatusLayoutManager.errorRetryViewId);
                    layoutSparseArray.put(id, view);
                    isShow = true;
                } else {
                    isShow = false;
                }
                break;
            case LAYOUT_EMPTYDATA_ID:
                if (mStatusLayoutManager.emptyDataVs != null) {
                    View view = mStatusLayoutManager.emptyDataVs.inflate();
                    if (mStatusLayoutManager.emptyDataLayout != null) mStatusLayoutManager.emptyDataLayout.setView(view);
                    retryLoad(view, mStatusLayoutManager.emptyDataRetryViewId);
                    layoutSparseArray.put(id, view);
                    isShow = true;
                } else {
                    isShow = false;
                }
                break;
        }
        return isShow;
    }

6.2 处理重新加载逻辑

  • 最后看看重新加载方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    *  重试加载
    */
    private void retryLoad(View view, int id) {
        View retryView = view.findViewById(mStatusLayoutManager.retryViewId != 0 ? mStatusLayoutManager.retryViewId : id);
        if (retryView == null || mStatusLayoutManager.onRetryListener == null) return;
        retryView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mStatusLayoutManager.onRetryListener.onRetry();
            }
        });
    }

07.如何使用该封装库

  • 可以自由切换内容,空数据,异常错误,加载,网络错误等5种状态。父类BaseActivity直接暴露5中状态,方便子类统一管理状态切换,这里fragment的封装和activity差不多。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    /**
    * ================================================
    * 作    者:杨充
    * 版    本:1.0
    * 创建日期:2017/7/6
    * 描    述:抽取类
    * 修订历史:
    * ================================================
    */
    public abstract class BaseActivity extends AppCompatActivity {

        protected StatusLayoutManager statusLayoutManager;

        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_base_view);
            initStatusLayout();
            initBaseView();
            initToolBar();
            initView();
        }

    //子类必须重写该方法
        protected abstract void initStatusLayout();

        protected abstract void initView();

        /**
        * 获取到布局
        */
        private void initBaseView() {
            LinearLayout ll_main = (LinearLayout) findViewById(R.id.ll_main);
            ll_main.addView(statusLayoutManager.getRootLayout());
        }

        //正常展示数据状态
        protected void showContent() {
            statusLayoutManager.showContent();
        }

        //加载数据为空时状态
        protected void showEmptyData() {
            statusLayoutManager.showEmptyData();
        }

        //加载数据错误时状态
        protected void showError() {
            statusLayoutManager.showError();
        }

        //网络错误时状态
        protected void showNetWorkError() {
            statusLayoutManager.showNetWorkError();
        }

        //正在加载中状态
        protected void showLoading() {
            statusLayoutManager.showLoading();
        }
    }
  • 子类继承BaseActivity后,该如何操作呢?具体如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    @Override
    protected void initStatusLayout() {
    statusLayoutManager = StateLayoutManager.newBuilder(this)
    .contentView(R.layout.activity_main)
    .emptyDataView(R.layout.activity_emptydata)
    .errorView(R.layout.activity_error)
    .loadingView(R.layout.activity_loading)
    .netWorkErrorView(R.layout.activity_networkerror)
    .build();
    }

    //或者添加上监听事件
    @Override
    protected void initStatusLayout() {
        statusLayoutManager = StateLayoutManager.newBuilder(this)
                .contentView(R.layout.activity_content_data)
                .emptyDataView(R.layout.activity_empty_data)
                .errorView(R.layout.activity_error_data)
                .loadingView(R.layout.activity_loading_data)
                .netWorkErrorView(R.layout.activity_networkerror)
                .onRetryListener(new OnRetryListener() {
                    @Override
                    public void onRetry() {
                        //为重试加载按钮的监听事件
                    }
                })
                .onShowHideViewListener(new OnShowHideViewListener() {
                    @Override
                    public void onShowView(View view, int id) {
                        //为状态View显示监听事件
                    }

                    @Override
                    public void onHideView(View view, int id) {
                        //为状态View隐藏监听事件
                    }
                })
                .build();
    }

    //如何切换状态呢?
    showContent();
    showEmptyData();
    showError();
    showLoading();
    showNetWorkError();

    //或者这样操作也可以
    statusLayoutManager.showLoading();
    statusLayoutManager.showContent();
  • 那么如何设置状态页面的交互事件呢?当状态是加载数据失败时,点击可以刷新数据;当状态是无网络时,点击可以设置网络。代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**
    * 点击重新刷新
    */
    private void initErrorDataView() {
        statusLayoutManager.showError();
        LinearLayout ll_error_data = (LinearLayout) findViewById(R.id.ll_error_data);
        ll_error_data.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                initData();
                adapter.notifyDataSetChanged();
                showContent();
            }
        });
    }

    /**
    * 点击设置网络
    */
    private void initSettingNetwork() {
        statusLayoutManager.showNetWorkError();
        LinearLayout ll_set_network = (LinearLayout) findViewById(R.id.ll_set_network);
        ll_set_network.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent("android.settings.WIRELESS_SETTINGS");
                startActivity(intent);
            }
        });
    }
  • 那有些页面想要自定义指定的状态页面UI,又该如何操作呢?倘若有些页面想定制状态布局,也可以自由实现,很简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * 自定义加载数据为空时的状态布局
    */
    private void initEmptyDataView() {
        statusLayoutManager.showEmptyData();
        //此处是自己定义的状态布局
        statusLayoutManager.showLayoutEmptyData(R.layout.activity_emptydata);
        LinearLayout ll_empty_data = (LinearLayout) findViewById(R.id.ll_empty_data);
        ll_empty_data.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                initData();
                adapter.notifyDataSetChanged();
                showContent();
            }
        });
    }