什么是多状态加载布局
对于大多数 App 而言,项目中都有多状态加载 View 这种需求,如下图所示
对应到开发中,我们通常会开发一个对应的自定义 layout 用于根据页面不同的状态来显示不同的提示 view。
在项目中,我们大多会在开发初期就把这套 layout 框架写好,然后其他人的自己的开发过程中直接使用即可。如下所示:
<com.okada.StatusLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/ll_btn_container"
android:layout_margin="16dp"
android:background="@android:color/holo_blue_light">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:text="我是内容"
android:textSize="18sp"/>
</com.yuejia.student.StatusLayout>
实现原理
在实现 StatusLayout 时,首先选择继承一个 ViewGroup 作为自己的父类。然后把子 View 分为 4 个部分:内容视图、加载中、空视图、错误视图。根据不同的的状态,控制不同状态下的 layout 显示。
StatusLayout 作为一个父容器只关心自己的 ContentView,异常页面和加载页面甚至可能没有机会出现,所以为了提高性能,用了 ViewStub 去懒加载视图
代码实现
把子 View 分为 4 个部分:内容视图、加载中、空视图、错误视图。如下:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/contentLayoutContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ViewStub
android:id="@+id/viewStub_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/status_layout_container_loading"/>
<ViewStub
android:id="@+id/viewStub_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/status_layout_container_empty"/>
<ViewStub
android:id="@+id/viewStub_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/status_layout_container_error"/>
</merge>
核心代码
看一下
<com.okada.StatusLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/ll_btn_container"
android:layout_margin="16dp"
android:background="@android:color/holo_blue_light">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:text="我是内容"
android:textSize="18sp"/>
</com.yuejia.student.StatusLayout>
要让 TextView add 到 内容视图中,就需要重写 public void addView(View child, int index, ViewGroup.LayoutParams params) 方法。如下
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (mContentLayoutContainer == null) {
// 这时候 mContentLayoutContainer 还没有加载好,Status 目前正在做
// add mContentLayoutContainer 和另外 3 个 ViewStub 的工作
super.addView(child, index, params);
} else {
if (mContentLayoutContainer.getChildCount() == 0) {
// 把 TextView 加入到 mContentLayoutContainer 中
mContentLayoutContainer.addView(child, index, params);
} else {
// 内容视图下只允许有一个子视图,其他视图就不要加到内容视图进去了
super.addView(child, index, params);
}
}
}
完整代码
StatusLayout
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.FrameLayout;
import com.yuejia.student.R;
public class StatusLayout extends FrameLayout {
public static final int LOADING = 1;
public static final int EMPTY = 2;
public static final int CONTENT = 3;
public static final int ERROR = 4;
public StatusLayout(Context context) {
super(context);
init(context, null, 0);
}
public StatusLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
public StatusLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private int mLoadingLayoutResId;
private int mEmptyLayoutResId;
private int mErrorLayoutResId;
private View mLoadingLayout;
private View mEmptyLayout;
private View mErrorLayout;
private FrameLayout mContentLayoutContainer;
private FrameLayout mLoadingLayoutContainer;
private FrameLayout mEmptyLayoutContainer;
private FrameLayout mErrorLayoutContainer;
private void init(Context context, AttributeSet attrs, int defStyle) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StatusLayout);
try {
mLoadingLayoutResId = typedArray.getResourceId(R.styleable.StatusLayout_stl_loadingLayout,
R.layout.status_layout_loading);
mEmptyLayoutResId = typedArray.getResourceId(R.styleable.StatusLayout_stl_emptyLayout,
R.layout.status_layout_empty);
mErrorLayoutResId = typedArray.getResourceId(R.styleable.StatusLayout_stl_errorLayout,
R.layout.status_layout_error);
} finally {
typedArray.recycle();
}
LayoutInflater.from(context).inflate(R.layout.status_layout, this);
mContentLayoutContainer = (FrameLayout) findViewById(R.id.contentLayoutContainer);
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (mContentLayoutContainer == null) {
super.addView(child, index, params);
} else {
if (mContentLayoutContainer.getChildCount() == 0) {
mContentLayoutContainer.addView(child, index, params);
} else {
super.addView(child, index, params);
}
}
}
public void setMode(@StateLayoutMode int mode) {
switch (mode) {
case LOADING:
setLoadingLayoutVisibility(true);
setEmptyLayoutVisibility(false);
setErrorLayoutVisibility(false);
setContentLayoutVisibility(false);
break;
case EMPTY:
setEmptyLayoutVisibility(true);
setLoadingLayoutVisibility(false);
setErrorLayoutVisibility(false);
setContentLayoutVisibility(false);
break;
case CONTENT:
setContentLayoutVisibility(true);
setEmptyLayoutVisibility(false);
setLoadingLayoutVisibility(false);
setErrorLayoutVisibility(false);
break;
case ERROR:
setErrorLayoutVisibility(true);
setEmptyLayoutVisibility(false);
setLoadingLayoutVisibility(false);
setContentLayoutVisibility(false);
break;
}
}
public void setLoadingLayout(View loadingLayout) {
mLoadingLayout = loadingLayout;
}
public void setEmptyLayout(View emptyLayout) {
mEmptyLayout = emptyLayout;
}
public void setErrorLayout(View errorLayout) {
mErrorLayout = errorLayout;
}
private void setLoadingLayoutVisibility(boolean visible) {
if (mLoadingLayoutContainer == null) {
if (visible) {
ViewStub viewStub = (ViewStub) findViewById(R.id.viewStub_loading);
viewStub.inflate();
mLoadingLayoutContainer = (FrameLayout) findViewById(R.id.status_loading);
if (mLoadingLayout == null) {
LayoutInflater.from(getContext()).inflate(mLoadingLayoutResId, mLoadingLayoutContainer);
} else {
mLoadingLayoutContainer.addView(mLoadingLayout);
}
mLoadingLayoutContainer.setVisibility(View.VISIBLE);
}
} else {
mLoadingLayoutContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
private void setEmptyLayoutVisibility(boolean visible) {
if (mEmptyLayoutContainer == null) {
if (visible) {
ViewStub viewStub = (ViewStub) findViewById(R.id.viewStub_empty);
viewStub.inflate();
mEmptyLayoutContainer = (FrameLayout) findViewById(R.id.status_empty);
if (mEmptyLayout == null) {
LayoutInflater.from(getContext()).inflate(mEmptyLayoutResId, mEmptyLayoutContainer);
} else {
mEmptyLayoutContainer.addView(mEmptyLayout);
}
mEmptyLayoutContainer.setVisibility(View.VISIBLE);
}
} else {
mEmptyLayoutContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
private void setErrorLayoutVisibility(boolean visible) {
if (mErrorLayoutContainer == null) {
if (visible) {
ViewStub viewStub = (ViewStub) findViewById(R.id.viewStub_error);
viewStub.inflate();
mErrorLayoutContainer = (FrameLayout) findViewById(R.id.status_error);
if (mErrorLayout == null) {
LayoutInflater.from(getContext()).inflate(mErrorLayoutResId, mErrorLayoutContainer);
} else {
mErrorLayoutContainer.addView(mErrorLayout);
}
mErrorLayoutContainer.setVisibility(View.VISIBLE);
}
} else {
mErrorLayoutContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
private void setContentLayoutVisibility(boolean visible) {
mContentLayoutContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
使用注解定义状态,而不是使用枚举
import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@IntDef({
StatusLayout.LOADING,
StatusLayout.EMPTY,
StatusLayout.ERROR,
StatusLayout.CONTENT
})
@Retention(RetentionPolicy.SOURCE)
public @interface StateLayoutMode {
}
用到的布局文件
status_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/contentLayoutContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ViewStub
android:id="@+id/viewStub_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/status_layout_container_loading"/>
<ViewStub
android:id="@+id/viewStub_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/status_layout_container_empty"/>
<ViewStub
android:id="@+id/viewStub_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/status_layout_container_error"/>
</merge>
status_layout_container_loading.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/status_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
status_layout_container_error.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/status_error"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
status_layout_container_empty.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/status_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
status_layout_loading.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="加载中..."
android:textSize="18sp"/>
</LinearLayout>
status_layout_error.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="哎呀,发生了错误"
android:textSize="18sp"/>
</LinearLayout>
status_layout_empty.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="暂无数据"
android:textSize="18sp"/>
</LinearLayout>
使用
定义好布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_status_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.okada.StatusLayoutActivity">
<LinearLayout
android:id="@+id/ll_btn_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Content"/>
<Button
android:id="@+id/btn_loading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Loading"/>
<Button
android:id="@+id/btn_empty"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Empty"/>
<Button
android:id="@+id/btn_error"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Error"/>
</LinearLayout>
<com.okada.StatusLayout
android:id="@+id/statusLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/ll_btn_container"
android:layout_margin="16dp"
android:background="@android:color/holo_blue_light">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:text="我是内容"
android:textSize="18sp"/>
</com.okada.StatusLayout>
</RelativeLayout>
监听按钮,改变状态
public class StatusLayoutActivity extends AppCompatActivity implements View.OnClickListener {
private StatusLayout mStatusLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_status_layout);
mStatusLayout = (StatusLayout) findViewById(R.id.statusLayout);
View loadingView = getLayoutInflater().inflate(R.layout.abc_loading, null);
loadingView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(StatusLayoutActivity.this, "Loading", Toast.LENGTH_SHORT).show();
}
});
mStatusLayout.setLoadingLayout(loadingView);
View emptyView = getLayoutInflater().inflate(R.layout.abc_empty, null);
emptyView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(StatusLayoutActivity.this, "EMPTY", Toast.LENGTH_SHORT).show();
}
});
mStatusLayout.setEmptyLayout(emptyView);
View errorView = getLayoutInflater().inflate(R.layout.abc_error, null);
errorView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(StatusLayoutActivity.this, "ERROR", Toast.LENGTH_SHORT).show();
}
});
mStatusLayout.setErrorLayout(errorView);
findViewById(R.id.btn_content).setOnClickListener(this);
findViewById(R.id.btn_loading).setOnClickListener(this);
findViewById(R.id.btn_empty).setOnClickListener(this);
findViewById(R.id.btn_error).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_content:
mStatusLayout.setMode(StatusLayout.CONTENT);
break;
case R.id.btn_loading:
mStatusLayout.setMode(StatusLayout.LOADING);
break;
case R.id.btn_empty:
mStatusLayout.setMode(StatusLayout.EMPTY);
break;
case R.id.btn_error:
mStatusLayout.setMode(StatusLayout.ERROR);
break;
}
}
}