前言
写界面的时候,大概率总会考虑到以下几个界面?
- 加载数据时的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来代表当前显示的状态,代码如下:
var stateBeforeChangeListener: ((oldState: ViewState, newState: ViewState) -> Boolean)? = null
var stateAfterChangeListener: ((oldState: ViewState, newState: ViewState) -> Unit)? = null
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
showViewByState(previewState)
stateAfterChangeListener?.invoke(previewState, value)
}
}
}
好,接下来,按照思路来说,我们需要加载xml 中设置的布局,思路即为,根据引用加载到相关的布局之后,首先设置到views 中,之后调用addView 添加到容器中。
init{
val typeArray = context.obtainStyledAttributes(attrs, R.styleable.MultiStateView)
val layoutInflater = LayoutInflater.from(context)
defaultLayoutInflater(
ViewState.CONTENT,
R.styleable.MultiStateView_msv_contentView,
typeArray,
layoutInflater
)
...
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
) {
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 类型。
private fun checkContentView(view: View?) {
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 这里自己简单封装了一下,主要在循环执行的时候会好用一点。
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 函数,下面实现一下即可。
private fun showViewByState(previousState: ViewState) {
if (enableAnimateLayoutChanges) {
for (i in views.indices) {
if (i != currentState.ordinal && i != previousState.ordinal) {
views[i]?.visibility = View.GONE
}
}
animateView(obtainView(previousState))
return
}
for (i in views.indices) {
if (i == currentState.ordinal) {
views[i]?.visibility = View.VISIBLE
} else {
views[i]?.visibility = View.GONE
}
}
}
关于动画的处理,如下所示:
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 {
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代码地址????????????????????
最后
创作不易,如有帮助一键三连咯🙆?♀?。欢迎技术探讨噢!
|