前言
写界面的时候,大概率总会考虑到以下几个界面?
- 加载数据时的loading界面
- 数据加载出来的content界面
- 数据加载失败界面
- 无数据界面
- …
那么能不能统一写一个容器进行管理上述提到的布局呢?
在外部使用的时候只需要根据传入状态然后展示对应的布局即可。
思路
基于上面的使用要求,可以自定义一个多状态容器view,进行不同状态的容器添加、展示管理。
- 列举常用的状态:
加载中
、网络错误
、错误
、空数据
、正常内容
、其他...
- 不同状态的view可以通过状态来获取、控制、显示
- 不同的状态的view可以通过xml添加、也可以在程序中调用函数添加
- 可以在状态切换之前/之后监听,并且拦截是否真正的进行状态切换
- 其他优化,是否包含切换动画等等
MultiStateView 核心代码实现思路
首先选择容器是什么,我这里选择FrameLayout
,因为简单,减少onDraw
的绘制过程。
接下来考虑,首先需要可以通过xml添加对应状态的布局,所以需要定义attrs.xml
<declare-styleable name="MultiStateView">
<!-- 多状态view -->
<attr name="msv_contentView" format="reference" />
<attr name="msv_loadingView" format="reference" />
<attr name="msv_emptyView" format="reference" />
<attr name="msv_netErrorView" format="reference" />
<attr name="msv_otherErrorView" format="reference" />
<attr name="msv_customView" format="reference" />
<!-- 设置当前view状态 -->
<attr name="msv_currentViewState" format="enum">
<enum name="content" value="0" />
<enum name="loading" value="1" />
<enum name="empty" value="2" />
<enum name="net_error" value="3" />
<enum name="other_error" value="4" />
<enum name="custom" value="5" />
</attr>
<!-- view切换时是否使用默认动画 -->
<attr name="msv_enableAnimateChanges" format="boolean" />
</declare-styleable>
对应的在程序中需要不同状态的枚举类,用来区分不同状态的布局view。
enum class ViewState {
CONTENT,
LOADING,
EMPTY,
NET_ERROR,
OTHER_ERROR,
CUSTOM
}
好,分类做好了,我们怎么管理这些状态呢?并且每个状态和设置进来的view都有对应关系。
利用枚举类的ordinal
属性,它表示不同的枚举的序号,比如CONTENT
的ordinal
为0
,CUSTOM
的ordinal
为5
。
基于上述的概念,我们完全可以使用一个数组来管理,每个状态的ordinal
作为下标,数组真正储存View
,这样就优雅形成了映射关系。
另外我们还需要一个currentState来代表当前显示的状态,代码如下:
/**
* 状态监听,改变前
*
* @return true 会继续更改state false 不继续更改状态,状态还是oldState
*/
var stateBeforeChangeListener: ((oldState: ViewState, newState: ViewState) -> Boolean)? = null
/**
* 状态监听,改变后 此时state已经更改为newState
*/
var stateAfterChangeListener: ((oldState: ViewState, newState: ViewState) -> Unit)? = null
/**
* 储存不同状态的view.
* 根据enum直接定位到内部数组的索引
*/
val views = Array<View?>(ViewState.values().size) { null }
/**
* 当前状态设置
*/
var currentState = ViewState.CONTENT
set(value) {
val previewState = field
if (value != previewState) {
if (stateBeforeChangeListener?.invoke(previewState, value) != false) {
field = value
//根据状态进行view显示
showViewByState(previewState)
stateAfterChangeListener?.invoke(previewState, value)
}
}
}
好,接下来,按照思路来说,我们需要加载xml
中设置的布局,思路即为,根据引用加载到相关的布局之后,首先设置到views
中,之后调用addView
添加到容器中。
init{
//初始化 配置的布局
val typeArray = context.obtainStyledAttributes(attrs, R.styleable.MultiStateView)
val layoutInflater = LayoutInflater.from(context)
//CONTENT布局
defaultLayoutInflater(
ViewState.CONTENT,
R.styleable.MultiStateView_msv_contentView,
typeArray,
layoutInflater
)
//其他布局填充
...
//获取并设置默认的viewState 若无默认设置则为 ViewState.CONTENT
currentState = when (typeArray.getInt(
R.styleable.MultiStateView_msv_currentViewState,
ViewState.CONTENT.ordinal
)) {
ViewState.CONTENT.ordinal -> ViewState.CONTENT
ViewState.LOADING.ordinal -> ViewState.LOADING
ViewState.EMPTY.ordinal -> ViewState.EMPTY
ViewState.NET_ERROR.ordinal -> ViewState.NET_ERROR
ViewState.OTHER_ERROR.ordinal -> ViewState.OTHER_ERROR
ViewState.CUSTOM.ordinal -> ViewState.CUSTOM
else -> ViewState.CONTENT
}
//切换动画开关
enableAnimateLayoutChanges =
typeArray.getBoolean(R.styleable.MultiStateView_msv_enableAnimateChanges, false)
}
/**
* 封装一个方法,做默认布局填充动作
*/
private fun defaultLayoutInflater(
state: ViewState,
@StyleableRes res: Int,
typeArray: TypedArray,
layoutInflater: LayoutInflater
) {
//view被数组持有 可以利用索引查找
typeArray.getResourceId(res, View.NO_ID)
.takeIf { it != View.NO_ID }?.let {
//正确获取到布局之后
val contentView = layoutInflater.inflate(it, this, false)
//不为空,先赋值到数组,并且加入布局
views[state.ordinal] = contentView
addView(contentView)
}
}
不过针对addView
这里需要注意一点,因为我们的容器是ViewGroup
,也可以直接利用xml
包含的形式设置子布局,这么就不会经过我们设置的状态进而被views
持有,所以需要在addView的时候做一次校验,如果CONTENT
为null,且不是其他状态View
,则默认分配给CONTENT
类型。
/**
* 判断如果不是ViewState中除了contentView以外定义的View,将此view默认设置为contentView
*/
private fun checkContentView(view: View?) {
// 如果contentView为null,且将要add的view不等于其他view,则将其默认赋值给contentview
ViewState.values()
.superReduce<Pair<Boolean, Boolean>, ViewState> { lastValue, currentViewState ->
var isNeedSetContent = lastValue ?: false to true
if (currentViewState == ViewState.CONTENT && obtainView(currentViewState) == null) {
isNeedSetContent = true to isNeedSetContent.second
}
if (obtainView(currentViewState) === view) {
isNeedSetContent = isNeedSetContent.first to false
}
isNeedSetContent
}.takeIf { it != null && it.first && it.first == it.second }?.let {
views[ViewState.CONTENT.ordinal] = view
if (currentState != ViewState.CONTENT) {
obtainView(ViewState.CONTENT)?.visibility = View.GONE
}
}
}
superReduce
这里自己简单封装了一下,主要在循环执行的时候会好用一点。
/**
* 仿照_Arrays库,允许返回非Array本身的类型
*/
inline fun <S, T> Array<out T>.superReduce(operation: (lastValue: S?, T) -> S?): S? {
if (isEmpty())
throw UnsupportedOperationException("Empty array can't be reduced.")
var accumulator: S? = null
for (index in 1..lastIndex) {
accumulator = operation(accumulator, this[index])
}
return accumulator
}
接下来就是设置currentState
的时候,对于View
的显示控制了,在上面的代码中已经出现了一个showViewByState
函数,下面实现一下即可。
/**
* 根据state显示相关的view
* @param state View状态
*/
private fun showViewByState(previousState: ViewState) {
if (enableAnimateLayoutChanges) {
//其他View全部隐藏
for (i in views.indices) {
if (i != currentState.ordinal && i != previousState.ordinal) {
views[i]?.visibility = View.GONE
}
}
//使用animate执行previousView与currentView的切换
animateView(obtainView(previousState))
return
}
//不执行动画
for (i in views.indices) {
if (i == currentState.ordinal) {
views[i]?.visibility = View.VISIBLE
} else {
views[i]?.visibility = View.GONE
}
}
}
关于动画的处理,如下所示:
/**
* 动画执行隐藏显示
* @param view view
*/
private fun animateView(previousView: View?) {
if (previousView == null) {
obtainView(currentState)?.let { it.visibility = View.VISIBLE }
?: throw IllegalStateException("当前状态的view不能为null")
return
}
val animateDuration = 200L
ObjectAnimator.ofFloat(previousView, "alpha", 1.0F, 0.0F).apply {
duration = animateDuration
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
previousView.visibility = View.VISIBLE
}
override fun onAnimationEnd(animation: Animator?) {
previousView.visibility = View.GONE
obtainView(currentState)?.let {
//当前View显示
ObjectAnimator.ofFloat(it, "alpha", 0.0F, 1.0F)
.apply { duration = animateDuration }.start()
} ?: throw IllegalStateException("当前状态的view不能为null")
}
})
}.start()
}
简单使用测试如下,至于测试的逻辑代码这里就不写了,下面提供git代码地址,感兴趣的可以找找呀。
<?xml version="1.0" encoding="utf-8"?>
<com.pumpkin.mvvm.widget.MultiStateView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:msv_emptyView="@layout/layout_test_empty"
app:msv_loadingView="@layout/layout_test_loading"
app:msv_netErrorView="@layout/layout_test_neterror"
app:msv_enableAnimateChanges="true"
app:msv_currentViewState="empty">
<include
android:id="@+id/i_normal"
layout="@layout/layout_test_normal" />
</com.pumpkin.mvvm.widget.MultiStateView>
以上即为核心实现逻辑代码。
附上完整GIT代码地址➡️➡️➡️➡️➡️➡️➡️➡️➡️➡️
最后
创作不易,如有帮助一键三连咯🙆♀️。欢迎技术探讨噢!