作者: Jooyer, 时间: 2018.08.08
,欢迎点赞,fork其实一般的APP 都是这么几个状态,本次效果和其他开源没有什么区别,只是在方法数量和类数量上,和我之前博客一样,比较少! 这是自我吹捧,哈哈!
只需要三个类 + 几个 XML 文件即可,即拷即用
接下来我们依次讲解:
- StatusManager
- RootStatusLayout
- OnRetryListener
- 其他几个 XML 文件
首先我们看看 StatusManager:
/** * Desc: 视图管理器 * Author: Jooyer * Date: 2018-07-30 * Time: 11:46 */class StatusManager(builder: Builder) { var mContext: Context var mNetworkErrorVs: ViewStub? = null var mNetworkErrorView:View? = null var mNetWorkErrorRetryViewId: Int = 0 var mEmptyDataVs: ViewStub? = null var mEmptyDataView:View? = null var mEmptyDataRetryViewId: Int = 0 var mErrorVs: ViewStub? = null var mErrorView:View? = null var mErrorRetryViewId: Int = 0 var mLoadingLayoutResId: Int = 0 var mContentLayoutResId: Int = 0 var mRetryViewId: Int = 0 var mContentLayoutView: View? = null var mRootFrameLayout: RootStatusLayout? = null /** * 显示loading */ fun showLoading() { mRootFrameLayout?.showLoading() } /** * 显示内容 */ fun showContent() { mRootFrameLayout?.showContent() } /** * 显示空数据 */ fun showEmptyData() { mRootFrameLayout?.showEmptyData() } /** * 显示网络异常 */ fun showNetWorkError() { mRootFrameLayout?.showNetWorkError() } /** * 显示异常 */ fun showError() { mRootFrameLayout?.showError() } /** * 得到root 布局 */ fun getRootLayout(): View { return mRootFrameLayout!! } class Builder(val context: Context) { var loadingLayoutResId: Int = 0 var contentLayoutResId: Int = 0 var contentLayoutView: View? = null var netWorkErrorVs: ViewStub? = null var netWorkErrorRetryViewId: Int = 0 var emptyDataVs: ViewStub? = null var emptyDataRetryViewId: Int = 0 var errorVs: ViewStub? = null var errorRetryViewId: Int = 0 var retryViewId: Int = 0 // var onShowHideViewListener: OnShowOrHideViewListener? = null var onRetryListener: OnRetryListener? = null fun loadingView(@LayoutRes loadingLayoutResId: Int): Builder { this.loadingLayoutResId = loadingLayoutResId return this } fun netWorkErrorView(@LayoutRes newWorkErrorId: Int): Builder { netWorkErrorVs = ViewStub(context) netWorkErrorVs!!.layoutResource = newWorkErrorId return this } fun emptyDataView(@LayoutRes noDataViewId: Int): Builder { emptyDataVs = ViewStub(context) emptyDataVs!!.layoutResource = noDataViewId return this } fun errorView(@LayoutRes errorViewId: Int): Builder { errorVs = ViewStub(context) errorVs!!.layoutResource = errorViewId return this } fun contentView(contentLayoutView: View): Builder { this.contentLayoutView = contentLayoutView return this } fun contentViewResId(@LayoutRes contentLayoutResId: Int): Builder { this.contentLayoutResId = contentLayoutResId return this } fun netWorkErrorRetryViewId(netWorkErrorRetryViewId: Int): Builder { this.netWorkErrorRetryViewId = netWorkErrorRetryViewId return this } fun emptyDataRetryViewId(emptyDataRetryViewId: Int): Builder { this.emptyDataRetryViewId = emptyDataRetryViewId return this } fun errorRetryViewId(errorRetryViewId: Int): Builder { this.errorRetryViewId = errorRetryViewId return this } fun retryViewId(retryViewId: Int): Builder { this.retryViewId = retryViewId return this } fun onRetryListener(onRetryListener: OnRetryListener): Builder { this.onRetryListener = onRetryListener return this } fun build(): StatusManager { return StatusManager(this) } } companion object { fun newBuilder(context: Context): Builder { return Builder(context) } } init { mContext = builder.context mLoadingLayoutResId = builder.loadingLayoutResId mNetworkErrorVs = builder.netWorkErrorVs mNetWorkErrorRetryViewId = builder.netWorkErrorRetryViewId mEmptyDataVs = builder.emptyDataVs mEmptyDataRetryViewId = builder.emptyDataRetryViewId mErrorVs = builder.errorVs mErrorRetryViewId = builder.errorRetryViewId mContentLayoutResId = builder.contentLayoutResId mRetryViewId = builder.retryViewId mContentLayoutView = builder.contentLayoutView mRootFrameLayout = RootStatusLayout(mContext) mRootFrameLayout!!.setStatusManager(this) mRootFrameLayout!!.setOnRetryListener(builder.onRetryListener) }}复制代码
使用建造者模式, 初始化必要的布局信息和视图控件,同时使用 ViewStub 优化异常View
然后我们看看 RootStatusLayout :
/** * Desc: 视图管理布局控件 * Author: Jooyer * Date: 2018-07-30 * Time: 11:23 */class RootStatusLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : ConstraintLayout(context, attrs, defStyleAttr) { constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context?) : this(context, null, 0) /** * loading 加载id */ val LAYOUT_LOADING_ID = 1 /** * 内容id */ val LAYOUT_CONTENT_ID = 2 /** * 异常id */ val LAYOUT_ERROR_ID = 3 /** * 网络异常id */ val LAYOUT_NETWORK_ERROR_ID = 4 /** * 空数据id */ val LAYOUT_EMPTY_ID = 5 /** * 存放布局集合 */ private val mLayoutViews = SparseArray() /** * 视图管理器 */ private var mStatusLayoutManager: StatusManager? = null /** * 不同视图的切换 */// private var onShowHideViewListener: OnShowOrHideViewListener? = null /** * 点击重试按钮回调 */ private var onRetryListener: OnRetryListener? = null fun setStatusManager(manager: StatusManager) { mStatusLayoutManager = manager addAllLayoutViewsToRoot() } private fun addAllLayoutViewsToRoot() { val params = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) if (0 != mStatusLayoutManager?.mContentLayoutResId) { addLayoutResId(mStatusLayoutManager?.mContentLayoutResId!!, LAYOUT_CONTENT_ID, params) } else if (null != mStatusLayoutManager?.mContentLayoutView) { addLayoutView(mStatusLayoutManager?.mContentLayoutView!!, LAYOUT_CONTENT_ID, params) } if (0 != mStatusLayoutManager?.mLoadingLayoutResId) { addLayoutResId(mStatusLayoutManager?.mLoadingLayoutResId!!, LAYOUT_LOADING_ID, params) } if (null != mStatusLayoutManager?.mEmptyDataVs) { addView(mStatusLayoutManager?.mEmptyDataVs, params) } if (null != mStatusLayoutManager?.mErrorVs) { addView(mStatusLayoutManager?.mErrorVs, params) } if (null != mStatusLayoutManager?.mNetworkErrorVs) { addView(mStatusLayoutManager?.mNetworkErrorVs, params) } } private fun addLayoutView(layoutView: View, layoutId: Int, param: ViewGroup.LayoutParams) { mLayoutViews.put(layoutId, layoutView) addView(layoutView, param) } private fun addLayoutResId(@LayoutRes layoutResId: Int, layoutId: Int, param: ViewGroup.LayoutParams) { val view: View = LayoutInflater.from(context) .inflate(layoutResId, null) mLayoutViews.put(layoutId, view) if (LAYOUT_LOADING_ID == layoutId) { view.visibility = View.GONE } addView(view, param) } /** * 显示loading */ fun showLoading() { if (mLayoutViews.get(LAYOUT_LOADING_ID) != null) showHideViewById(LAYOUT_LOADING_ID) } /** * 显示内容 */ fun showContent() { if (mLayoutViews.get(LAYOUT_CONTENT_ID) != null) showHideViewById(LAYOUT_CONTENT_ID) } /** * 显示空数据 */ fun showEmptyData() { if (inflateLayout(LAYOUT_EMPTY_ID)) showHideViewById(LAYOUT_EMPTY_ID) } /** * 显示网络异常 */ fun showNetWorkError() { if (inflateLayout(LAYOUT_NETWORK_ERROR_ID)) showHideViewById(LAYOUT_NETWORK_ERROR_ID) } /** * 显示异常 */ fun showError() { if (inflateLayout(LAYOUT_ERROR_ID)) showHideViewById(LAYOUT_ERROR_ID) } private fun showHideViewById(layoutId: Int) { for (i in 0 until mLayoutViews.size()) { val key = mLayoutViews.keyAt(i) val value = mLayoutViews[key] // 显示该 View if (layoutId == key) { value.visibility = View.VISIBLE } else { if (View.GONE != value.visibility) { value.visibility = View.GONE } } } } fun setOnRetryListener(listener: OnRetryListener?) { onRetryListener = listener } /** * 加载 StubView */ private fun inflateLayout(layoutId: Int): Boolean { var isShow = true when (layoutId) { LAYOUT_NETWORK_ERROR_ID -> { isShow = when { null != mStatusLayoutManager?.mNetworkErrorView -> { retryLoad(mStatusLayoutManager?.mNetworkErrorView!!, mStatusLayoutManager?.mNetWorkErrorRetryViewId!!) mLayoutViews.put(layoutId, mStatusLayoutManager?.mNetworkErrorView!!) return true } null != mStatusLayoutManager?.mNetworkErrorVs -> { val view: View = mStatusLayoutManager?.mNetworkErrorVs!!.inflate() mStatusLayoutManager?.mNetworkErrorView = view retryLoad(view, mStatusLayoutManager?.mNetWorkErrorRetryViewId!!) mLayoutViews.put(layoutId, view) true } else -> false } } LAYOUT_ERROR_ID -> { isShow = when { null != mStatusLayoutManager?.mErrorView -> { retryLoad(mStatusLayoutManager?.mErrorView!!, mStatusLayoutManager?.mErrorRetryViewId!!) mLayoutViews.put(layoutId, mStatusLayoutManager?.mErrorView!!) return true } null != mStatusLayoutManager?.mErrorVs -> { val view: View = mStatusLayoutManager?.mErrorVs!!.inflate() mStatusLayoutManager?.mErrorView = view retryLoad(view, mStatusLayoutManager?.mErrorRetryViewId!!) mLayoutViews.put(layoutId, view) true } else -> false } } LAYOUT_EMPTY_ID -> { isShow = when { null != mStatusLayoutManager?.mEmptyDataView -> { retryLoad(mStatusLayoutManager?.mEmptyDataView!!, mStatusLayoutManager?.mEmptyDataRetryViewId!!) mLayoutViews.put(layoutId, mStatusLayoutManager?.mEmptyDataView!!) return true } null != mStatusLayoutManager?.mEmptyDataVs -> { val view: View = mStatusLayoutManager?.mEmptyDataVs!!.inflate() mStatusLayoutManager?.mEmptyDataView = view retryLoad(view, mStatusLayoutManager?.mEmptyDataRetryViewId!!) mLayoutViews.put(layoutId, view) true } else -> false } } } return isShow } /** * 加载重试按钮,并绑定监听 */ private fun retryLoad(view: View, layoutResId: Int) { val retryView: View? = view.findViewById( if (0 != mStatusLayoutManager?.mRetryViewId!!) { mStatusLayoutManager?.mRetryViewId!! } else { layoutResId }) ?: return retryView?.setOnClickListener { onRetryListener?.onRetry() } }}复制代码
加载必要是视图到布局中,并根据需要显示和隐藏相关 View,逻辑也是很简单,哈哈!
接着看看最后一个类,其实就是个回调...
/** * Desc: 数据异常处理时点击回调 * Author: Jooyer * Date: 2018-07-30 * Time: 11:22 */interface OnRetryListener{ fun onRetry()}复制代码
这个也占了一个类,我喜欢这样,两个字 --> 任性
最后就是几个我就一股脑都抛出来了,准备接招!!! 你喜欢可以随意定制,咋舒服咋来.
widget_empty_page.xml -----> 没有数据时使用
复制代码
widget_error_page.xml -----> 异常(数据解析异常,服务器500等) 时使用
复制代码
widget_nonetwork_page.xml -----> 网络异常时使用
复制代码
widget_progress_bar.xml ----->加载数据时使用
复制代码
以上就是全部了,如果小伙伴发现有不能运行的,请看下面这个文件,它是在 values 内哦
dimens.xml -----> 定义了视图大小
复制代码 100dp 100dp 18dp 10dp 6dp 16sp
这次真的是全部了, 下面来介绍用法.
一般我们都需要定义基类,所以本次的视图管理器也在基类中,请看:
BaseActivity
/** * Desc: Activity 基类 * Author: Jooyer * Date: 2018-08-04 * Time: 21:22 */abstract class BaseActivity :AppCompatActivity(), OnRetryListener { /** * 请求网络异常等界面管理 */ var mStatusManager: StatusManager? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (0 != getLayoutId()) { setContentView(initStatusManager(savedInstanceState)) } } /** * Activity 布局文件 */ abstract fun getLayoutId(): Int /** * 初始化 View */ abstract fun initializedViews(savedInstanceState: Bundle?, contentView: View) private fun initStatusManager(savedInstanceState: Bundle?): View { if (0 != getLayoutId()) { val contentView = LayoutInflater.from(this) .inflate(getLayoutId(), null) initializedViews(savedInstanceState, contentView) return if (useStatusManager()) { initialized(contentView) } else { contentView.visibility = View.VISIBLE contentView } } throw IllegalStateException("getLayoutId() 必须调用,且返回正常的布局ID") } private fun initialized(contentView: View): View { mStatusManager = StatusManager.newBuilder(this) .contentView(contentView) .loadingView(R.layout.widget_progress_bar) .emptyDataView(R.layout.widget_empty_page) .netWorkErrorView(R.layout.widget_nonetwork_page) .errorView(R.layout.widget_error_page) .retryViewId(R.id.tv_retry_status) // 注意以上布局中如果有重试ID,则必须一样,ID名称随意,记得这里填写正确 .onRetryListener(this) .build() mStatusManager?.showLoading() return mStatusManager?.getRootLayout()!! } /** * 是否使用视图布局管理器,默认不使用 */ open fun useStatusManager(): Boolean { return false } /** * 点击视图中重试按钮 */ override fun onRetry() { Toast.makeText(this,"如果需要点击重试,则重写 onRetry() 方法", Toast.LENGTH_SHORT).show() }}复制代码
继续
BaseFragment
/** * Desc: Fragment 基类 * Author: Jooyer * Date: 2018-08-04 * Time: 21:23 */abstract class BaseFragment: Fragment(), OnRetryListener { /** * 请求网络异常等界面管理 */ var mStatusManager: StatusManager? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return initStatusManager(inflater, container, savedInstanceState) } /** * Fragment 布局文件 */ abstract fun getLayoutId(): Int abstract fun initializedViews(savedInstanceState: Bundle?, contentView: View) /** * 此函数开始数据加载的操作,且仅调用一次 * 主要是加载动画,初始化展示数据的布局 */ private fun initStatusManager(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { if (0 != getLayoutId()) { val contentView = inflater.inflate(getLayoutId(), container, false) initializedViews(savedInstanceState, contentView) return if (useStatusManager()) { initialized(contentView) } else { contentView.visibility = View.VISIBLE contentView } } throw IllegalStateException("getLayoutId() 必须调用,且返回正常的布局ID") } private fun initialized(contentView: View): View { mStatusManager = StatusManager.newBuilder(contentView.context) .contentView(contentView) .loadingView(R.layout.widget_progress_bar) .emptyDataView(R.layout.widget_empty_page) .netWorkErrorView(R.layout.widget_nonetwork_page) .errorView(R.layout.widget_error_page) .retryViewId(R.id.tv_retry_status) // 注意以上布局中如果有重试ID,则必须一样,ID名称随意,记得这里填写正确 .onRetryListener(this) .build() mStatusManager?.showLoading() return mStatusManager?.getRootLayout()!! } /** * 是否使用视图布局管理器,默认不使用 */ open fun useStatusManager(): Boolean { return false } /** * 点击视图中重试按钮 */ override fun onRetry() { Toast.makeText(this,"如果需要点击重试,则重写 onRetry() 方法", Toast.LENGTH_SHORT).show() }}复制代码
PS: onRetry() 的 Toast 仅仅演示用的,具体逻辑,请重写此方法来处理...
最后看看在 Activity 的用法吧, Fragment 类似,如果小伙伴在 Fragment 中不会使用或者有其他疑问请留言!
class MainActivity : BaseActivity() { override fun getLayoutId(): Int { return R.layout.activity_main } // 仅仅演示如何通过 findViewById 找到控件,其实 Kotlin 不用这么麻烦 override fun initializedViews(savedInstanceState: Bundle?, contentView: View) { val tv_main_activity = contentView.findViewById(R.id.tv_main_activity) } // 如果使用 StateManager 必须重写下面方法,且返回 true // 如果返回 true 则会显示加载的 loading override fun useStatusManager(): Boolean { return true } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.state_menu, menu) return true } // 下面展示了显示各个状态的方式 override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.option_empty_page -> { mStatusManager?.showEmptyData() return true } R.id.option_error_page -> { mStatusManager?.showError() return true } R.id.option_loading_page -> { mStatusManager?.showLoading() return true } R.id.option_network_page -> { mStatusManager?.showNetWorkError() return true } R.id.option_content_page -> { mStatusManager?.showContent() return true } else -> return super.onOptionsItemSelected(item) } }// 这个仅仅演示加载 Framgment ,和本例没有什么关系 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportFragmentManager.beginTransaction() .add(R.id.fl_container,MainFragment()) .commit() }}复制代码
哈哈,我觉得很简单啊,小伙伴们觉得呢?虽然简单不过它可以满足基本的需求哦!还可以随意定制!喜欢记得点赞,收藏,转发哈!
膜拜的大神:
实在抱歉,记得有看到过大神有类似思路的,只是想不起了,如果大神看到,请通知我,我会在这里写上大神的博客