适用范围

主要适用于Acitivty/Fragment需要根据不同的状态来展示对应的状态页面。


设计思路

  • 设计一个布局管理器,用于管理各个状态的布局显示/隐藏
  • 在布局管理器的根layout文件中,通过ViewStubCompat导入对应状态的layout
  • 通过getViewGroup方法获取对应调用该管理类调用者的根Layout——即数据正常展示的UI
  • 根据State进行对应的Page显示/隐藏

实现后的StatusViewController.java类

package com.dohenes.common.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.ViewStubCompat;

import com.dohenes.common.R;

/**
 * ClassName StatusViewController
 * Describe TODO<状态试图控制器>
 * Author jgduan
 * Date 2019/12/16 11:09
 * Version v1.0
 */
public abstract class StatusViewController extends LinearLayout {
    private static final String TAG = StatusViewController.class.getSimpleName();

    /**
     * 状态枚举类,对应不同的状态
     * Empty对应空数据内容提示
     * Load对应加载中内容提示
     * Error对应发生异常提示
     * NoNetwork对应无网络提示
     * Success对应的是调用者自身的初始布局页面
     */
    public enum State {
        Empty, Load, Error, NoNetwork, Success
    }

    private View mEmptyView;// 空数据提示View
    private View mErrorView;// 异常提示View
    private View mLoadingView;// 加载中View
    private View mNoNetworkView;// 无网络提示View
    private ViewGroup mSuccessView;// 这个实际上是对标调用StatusViewController的Activity/Fragment根View
    private View mTitle;// Activity设置的Title

    public StatusViewController(@NonNull Context context) {
        super(context);
        init(context);
    }

    public StatusViewController(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    /**
     * 一些初始化的操作
     *
     * @param context context
     */
    private void init(@NonNull Context context) {
        setOrientation(VERTICAL);
        inflate(context, R.layout.status_view, this);// 引入多状态布局layout
        mSuccessView = getViewGroup();// 获取调用者自身的初始View
        mTitle = mSuccessView.findViewWithTag("title");
        this.addView(mSuccessView, ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);// 添加默认的初始View进来
    }

    /**
     * 开始载入数据
     */
    private void loadData() {
        showPageWithState(State.Load);
        onLoadData();// 通知调用的Activity这里触发了载入数据操作,需要它配合做出处理
    }

    /**
     * 自定义的点击事件,只针对重新加载按钮
     */
    private OnClickListener mRetryClickListener = new OnClickListener() {

        @Override
        public void onClick(View v) {
            loadData();
        }
    };

    /**
     * 获取调用该控制器Activity/Fragment的根ViewGroup
     *
     * @return ViewGroup
     */
    public abstract ViewGroup getViewGroup();

    /**
     * 载入数据回调
     */
    public abstract void onLoadData();

    /**
     * 根据状态类型展示对应的页面
     *
     * @param status State
     */
    @SuppressLint("RestrictedApi")
    public void showPageWithState(State status) {
        if (mTitle != null) {
            String tag = (String) getChildAt(0).getTag();
            if (!TextUtils.equals(tag, "title")) {// 没有标题栏
                mSuccessView.removeView(mTitle);
                addView(mTitle, 0);
            }
        }
        switch (status) {
            case Success:
                if (mLoadingView != null) {
                    mLoadingView.setVisibility(View.GONE);
                }
                if (mErrorView != null) {
                    mErrorView.setVisibility(View.GONE);
                }
                if (mNoNetworkView != null) {
                    mNoNetworkView.setVisibility(View.GONE);
                }
                if (mEmptyView != null) {
                    mEmptyView.setVisibility(View.GONE);
                }
                mSuccessView.setVisibility(View.VISIBLE);
                break;
            case Empty:
                mSuccessView.setVisibility(View.GONE);
                if (mLoadingView != null) {
                    mLoadingView.setVisibility(View.GONE);
                }
                if (mErrorView != null) {
                    mErrorView.setVisibility(View.GONE);
                }
                if (mNoNetworkView != null) {
                    mNoNetworkView.setVisibility(View.GONE);
                }
                if (mEmptyView == null) {
                    ViewStubCompat viewStub = findViewById(R.id.svc_stub_empty);
                    mEmptyView = viewStub.inflate();
                } else {
                    mEmptyView.setVisibility(View.VISIBLE);
                }
                mEmptyView.findViewById(R.id.vcs_btn_empty)
                        .setOnClickListener(mRetryClickListener);
                break;
            case Load:
                mSuccessView.setVisibility(View.GONE);
                if (mEmptyView != null) {
                    mEmptyView.setVisibility(View.GONE);
                }
                if (mErrorView != null) {
                    mErrorView.setVisibility(View.GONE);
                }
                if (mNoNetworkView != null) {
                    mNoNetworkView.setVisibility(View.GONE);
                }
                if (mLoadingView == null) {
                    ViewStubCompat viewStub = findViewById(R.id.svc_stub_loading);
                    mLoadingView = viewStub.inflate();
                } else {
                    mLoadingView.setVisibility(View.VISIBLE);
                }
                break;
            case Error:
                mSuccessView.setVisibility(View.GONE);
                if (mEmptyView != null) {
                    mEmptyView.setVisibility(View.GONE);
                }
                if (mLoadingView != null) {
                    mLoadingView.setVisibility(View.GONE);
                }
                if (mNoNetworkView != null) {
                    mNoNetworkView.setVisibility(View.GONE);
                }
                if (mErrorView == null) {
                    ViewStubCompat viewStub = findViewById(R.id.svc_stub_error);
                    mErrorView = viewStub.inflate();
                } else {
                    mErrorView.setVisibility(View.VISIBLE);
                }
                mErrorView.findViewById(R.id.vcs_btn_error)
                        .setOnClickListener(mRetryClickListener);
                break;
            case NoNetwork:
                mSuccessView.setVisibility(View.GONE);
                if (mEmptyView != null) {
                    mEmptyView.setVisibility(View.GONE);
                }
                if (mErrorView != null) {
                    mErrorView.setVisibility(View.GONE);
                }
                if (mLoadingView != null) {
                    mLoadingView.setVisibility(View.GONE);
                }
                if (mNoNetworkView == null) {
                    ViewStubCompat viewStub = findViewById(R.id.svc_stub_no_network);
                    mNoNetworkView = viewStub.inflate();
                } else {
                    mNoNetworkView.setVisibility(View.VISIBLE);
                }
                mNoNetworkView.findViewById(R.id.vcs_btn_no_net)
                        .setOnClickListener(mRetryClickListener);
                break;
            default:
                break;
        }
    }

}

// 用法示例:修改baseActivity中的setContentView(getView());
//    private StatusViewController statusViewController;
//
//    private View getView() {
//        statusViewController = new StatusViewController(this) {
//            @Override
//            public ViewGroup getViewGroup() {
//                return getLayoutInflaterViewGroup();
//            }
//
//            @Override
//            public void onLoadData() {
//                // 这里需要在开始载入数据,暂时写个延迟模拟操作成功的效果
//                Observable.timer(3, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
//                        .subscribe(new Observer<Long>() {
//                            @Override
//                            public void onCompleted() {
//                                statusViewController.showPageWithState(State.Success);
//                            }
//
//                            @Override
//                            public void onError(Throwable e) {
//
//                            }
//
//                            @Override
//                            public void onNext(Long aLong) {
//
//                            }
//                        });
//            }
//        };
//        // 默认应该是Load状态,示例放上Empty状态,便于更直观的体验流程
//        statusViewController.showPageWithState(StatusViewController.State.Empty);
//        return statusViewController;
//    }
//
//    private ViewGroup getLayoutInflaterViewGroup() {
//        return (ViewGroup) View.inflate(this, getContentViewID(), null);
//    }

对应的布局代码

  • StatusViewController对应的根布局status_view.mxl
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <androidx.appcompat.widget.ViewStubCompat
        android:id="@+id/svc_stub_loading"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout="@layout/status_loading" />

    <androidx.appcompat.widget.ViewStubCompat
        android:id="@+id/svc_stub_empty"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout="@layout/status_empty" />

    <androidx.appcompat.widget.ViewStubCompat
        android:id="@+id/svc_stub_error"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout="@layout/status_error" />

    <androidx.appcompat.widget.ViewStubCompat
        android:id="@+id/svc_stub_no_network"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout="@layout/status_no_network" />
</merge>
  • status_empty.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <Button
        android:id="@+id/vcs_btn_empty"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/vsc_empty_tips" />

</LinearLayout>
  • status_error.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <Button
        android:id="@+id/vcs_btn_error"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/vcs_error_tips" />

</LinearLayout>
  • status_loading.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:gravity="center"
    android:orientation="vertical">

    <ProgressBar
        android:id="@+id/vcs_pb_loading"
        style="?android:attr/progressBarStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>
  • status_no_network.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:gravity="center"
    android:orientation="vertical">

    <Button
        android:id="@+id/vcs_btn_no_net"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/vcs_no_net_tips" />

</LinearLayout>
  • 对应的values值
    <string name="vsc_empty_tips">没有加载到数据,重新加载</string>
    <string name="vcs_error_tips">发生错误,点击重新加载</string>
    <string name="vcs_no_net_tips">没有网络,点击重试</string>

三方库推荐

张鸿洋_LoadingAndRetryManager
MultiPageControl