作者: Jooyer, 时间: 2018.08.08


其实一般的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<View>()
/**
* 视图管理器
*/
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 -----> 没有数据时使用
<?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"
android:visibility="visible">
<ImageView
android:id="@+id/empty_img_status"
android:layout_width="@dimen/width_100"
android:layout_height="@dimen/height_100"
android:background="@android:color/holo_blue_bright"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_10"
android:gravity="center"
android:textSize="@dimen/textSize_16"
android:text="暂时没有数据! "
/>
</LinearLayout>
widget_error_page.xml -----> 异常(数据解析异常,服务器500等) 时使用
<?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"
android:visibility="visible">
<ImageView
android:id="@+id/error_img_status"
android:layout_width="@dimen/width_100"
android:layout_height="@dimen/height_100"
android:background="@android:color/holo_orange_dark"
/>
<TextView
android:id="@+id/error_text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/padding_10"
android:layout_marginTop="@dimen/padding_10"
android:text="出错了..."
/>
<TextView
android:id="@+id/tv_retry_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:gravity="center"
android:paddingBottom="@dimen/padding_6"
android:paddingLeft="@dimen/padding_18"
android:paddingRight="@dimen/padding_18"
android:paddingTop="@dimen/padding_6"
android:text="点击重试! "
/>
</LinearLayout>
widget_nonetwork_page.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"
android:visibility="visible">
<ImageView
android:id="@+id/no_network_img_status"
android:layout_width="@dimen/width_100"
android:layout_height="@dimen/height_100"
android:background="@android:color/holo_green_dark"
/>
<TextView
android:id="@+id/no_network_text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/padding_10"
android:layout_marginTop="@dimen/padding_10"
android:text="网络错误!..."
/>
<TextView
android:id="@+id/tv_retry_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:gravity="center"
android:paddingBottom="@dimen/padding_6"
android:paddingLeft="@dimen/padding_18"
android:paddingRight="@dimen/padding_18"
android:paddingTop="@dimen/padding_6"
android:text="点击重试"
/>
</LinearLayout>
widget_progress_bar.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">
<ProgressBar
android:id="@+id/progress_bar_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
/>
</LinearLayout>
以上就是全部了,如果小伙伴发现有不能运行的,请看下面这个文件,它是在 values 内哦
dimens.xml -----> 定义了视图大小
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="width_100">100dp</dimen>
<dimen name="height_100">100dp</dimen>
<dimen name="padding_18">18dp</dimen>
<dimen name="padding_10">10dp</dimen>
<dimen name="padding_6">6dp</dimen>
<dimen name="textSize_16">16sp</dimen>
</resources>
这次真的是全部了, 下面来介绍用法.
一般我们都需要定义基类,所以本次的视图管理器也在基类中,请看:
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<TextView>(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()
}
}
哈哈,我觉得很简单啊,小伙伴们觉得呢?虽然简单不过它可以满足基本的需求哦!还可以随意定制!喜欢记得点赞,收藏,转发哈!
膜拜的大神:
实在抱歉,记得有看到过大神有类似思路的,只是想不起了,如果大神看到,请通知我,我会在这里写上大神的博客