支持自定义的状态布局 StateLayout

998 阅读5分钟

业务状态变更时页面的动态响应是一个非常基本实现逻辑,但在有些场景下,可能需要避免进行 Activity 跳转或是 Fragment 切换,为解决这个问题根据状态进行布局动态变更就十分有必要了。

通常为了解决数据页面加载时的四个状态:Loading(加载中)、Error (加载错误)、Empty(无内容)、Content(显示内容)的动态切换 ,我们会考虑在同个窗口中动态切换界面布局,我们姑且把这种根据业务状态动态切换视图的组件称为状态布局,其实已经有很多开源库实现了这个功能,但是对于支持添加自定义状态的比较少,因此 StateLayout 在此基础上加入了自定义业务状态的支持。好在虽然轮子重复造,但适合业务的才最重要。

比如在下面这个找回密码的流程中大致会有六种状态,这时如果能根据自定义的状态进行布局动态切换会变得就相当简单了。

按照这个思路来实现了一个支持自定义状态布局的 StateLayout。 下面时实现的 demo 效果,在同个 Activity 中根据业务状态进行切换布局:

Sample

项目地址:https://github.com/baishixian/StateLayout

如何使用:

1. 添加依赖

Project build.gradle

repositories {
    jcenter()
}

Module build.gradle

compile 'gdut.bsx:stateLayout:1.1.0'

2. 添加到布局中

在布局文件中使用该组件

<?xml version="1.0" encoding="utf-8"?>
<gdut.bsx.view.StateLayout
          android:id="@+id/state_layout"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          app:enableAnim="true"
          app:emptyContentLayoutResId="@layout/layout_empty"
          app:errorLayoutResId="@layout/layout_error"
          app:loadingLayoutResId="@layout/layout_loading">
          
     <include layout="@layout/layout_content"/>

</gdut.bsx.view.StateLayout>

3. 在代码中使用

private static final int STATE_STEP_1 = 1;
private static final int STATE_STEP_2 = 2;
    
StateLayout stateLayout = findViewById(R.id.state_layout);
// show content
stateLayout.showContent();
// show loading
stateLayout.showLoading();

// addItemLayout
stateLayout.addItemLayout(STATE_STEP_1, R.layout.layout_custom_step_1);
stateLayout.changeState(STATE_STEP_1);

// addLazyInflateStateLayout
stateLayout.addLazyInflateStateLayout(STATE_STEP_2, R.layout.layout_custom_step_2);
stateLayout.changeState(STATE_STEP_2);

说明

StateLayout 继承了 FrameLayout,可用于在同个窗口种或局部视图内按照不同业务状态切换不同布局。

一般情况下,建议在使用 xml 文件中定义 StateLayout 布局时在该布局标签下添加一个内容视图,作为默认的业务内容视图。

1. 比如在加载数据完成,需要展示内容视图时,我们可以调用:

/**
 * show content 从其他状态切换到展示内容状态
 * 展示 xml 布局文件中定义在该布局标签中的视图内容
 */
public void showContent()

2. 内置状态支持

StateLayout 按照一般业务需求,内置了三种基本状态:Error State, Loading State, Empty Content Statet。 通过 StateLayout 的自定义属性可以指定上面状态对应的布局样式:

    app:emptyContentLayoutResId="@layout/layout_empty"
    app:errorLayoutResId="@layout/layout_error"
    app:loadingLayoutResId="@layout/layout_loading"

内置的状态可以根据自身实际业务合理配置,未指定布局的状态不会进行加载,所以不会影响原本视图性能。

然后可在代码中进行切换:

/**
 * change to empty content state.
 */
public void showEmptyContent()

/**
 * change to error state.
 */
public void showError()

/**
 * change to empty content state.
 */
public void showLoading()

3. 页面重试加载支持

为方便在状态异常时引导用户即使重试刷新页面,在 Error State 和 Empty Content State 时支持重试点击事件监听,你可以指定布局中的某个控件作为点击触发重试的载体:

比如我们自定义一个加载错误页面,指定该页面的 button 点击后可以重试加载。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/error_state_layout"
             android:layout_width="match_parent"
             android:layout_height="match_parent">
   
  <!-- 指定布局中的某个控件作为点击触发重试的载体,注意使用 @id/ 的方式引用 StateLayout 提供的 id -->
  <Button
            android:id="@id/sl_error_retry"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Retry"/>
 </FrameLayout>

然后在代码中添加重试监听器:

// 绑定监听器
stateLayout.setOnEmptyRetryListener(new StateLayout.OnEmptyContentRetryListener() {
         @Override
         public void onEmptyContentRetry() {
             retryLoad();
         }
});

为方便设置重试监听,提供了下面两个 id 用于指定响应点击重试事件的组件:

  • Error State 对应关联的 id 为 sl_error_retry
  • Empty Content State 对应关联的 id 为 sl_empty_content_retry

StateLayout 提供了下面的监听器设置接口:

/**
 * 设置错误重试监听
 * @param listener 重试监听
 */
public void setOnErrorRetryListener(OnErrorRetryListener listener) 
 
 /**
  * 设置空内容重试监听
  * @param listener 重试监听
  */
public void setOnEmptyRetryListener(OnEmptyContentRetryListener listener)

4. 添加自定义的状态布局

StateLayout 支持状态布局拓展,可以根据自身业务需求添加自定义状态和对应布局,同时 StateLayout 也提供了布局懒加载的支撑,方便自由取舍业务需求和性能要求:

 /**
   * 添加状态布局并立即初始化
   */
public void addItemLayout(int state, int layoutId)
    
 /**
   * 添加懒加载的状态布局,会先缓存布局存根但并不立即初始化,只有切换到特定的状态时才初始化其对应布局,降低性能影响
   * 注意:如果使用该方法添加状态布局,需要在调用切换状态方法 changeState(int state) 后才能使用布局内的控件
   * @param state 状态
   * @param layoutId 布局 id
   */
 public void addLazyInflateStateLayout(int state, @LayoutRes int layoutId);

具体使用示例:

 // 指定两个自定义状态对应的 ID
 private static final int STATE_STEP_1 = 1;
 private static final int STATE_STEP_2 = 2;
 
 // add stateLayout item immediately
 // 添加自定义状态布局
 stateLayout.addItemLayout(STATE_STEP_1, R.layout.layout_custom_step_1);
 // 为布局控件添加监听
 findViewById(R.id.bt_step_1_next).setOnClickListener(listener);
 // 切换到自定义状态布局 STEP_1
 stateLayout.changeState(STATE_STEP_1);
 
 // add stateLayout item that will be lazy inflate
 // 懒添加状态布局
 // 注意:其布局加载是在 changeState(STATE_STEP_2) 时进行,调用该方法后还不能立即操作状态 STATE_STEP_2 布局的子控件
 stateLayout.addLazyInflateStateLayout(STATE_STEP_2, R.layout.layout_custom_step_2);
 // 切换到自定义布局状态,由于状态 STATE_STEP_2 是使用 addLazyInflateStateLayout 添加的,此时才开始加载布局
 stateLayout.changeState(STATE_STEP_2);
 // 布局状态切换完后,此时才可以操作该布局的子控件
 findViewById(R.id.bt_step_2_previous).setOnClickListener(listener);

5. 是否启用状态布局切换动画

 <!-- xml 布局内定义属性-->
 app:enableAnim="true"
public void enableAnim(boolean enable)