通常我们浏览新闻APP的时候,会有一个表示加载状态的控件,表示当前正在加载数据或是网络断开导致加载出错了,数据为空等,就比如下面的今日头条:
在这篇文章中,我将实现一个加载状态控件LoadingLayout。
首先,要明确一下这个加载控件会有几种状态,第一肯定是先要有一个正在加载的状态,加载中的状态可以用一个gif动图或是动画来实现;当加载完成请求到数据后又可以分为两种状态,有数据和无数据,有数据自然是要隐藏整个加载控件,无数据时要有一个表示无数据的状态,这就有三种状态了;但是还要考虑到数据加载出错的情况,比如网络断开了或是接口挂了,所以基本上是要有四种状态:加载中、隐藏、无数据、加载出错。
private const val STATE_LOADING = 0 //加载中
private const val STATE_EMPTY = 1 //无数据
private const val STATE_FAIL = 2 //加载失败
private const val STATE_HIDE = 3 //隐藏
本文实现的LoadingLayout会有下面的自定义属性:
<!--加载状态布局-->
<declare-styleable name="LoadingLayout">
<attr name="loadingSrc" format="reference" />
<attr name="loadingDescText" format="string" />
<attr name="emptySrc" format="reference" />
<attr name="emptyDescText" format="string" />
<attr name="emptyActionText" format="string" />
<attr name="failSrc" format="reference" />
<attr name="failDescText" format="string" />
<attr name="failActionText" format="string" />
<attr name="intercept" format="boolean" />
</declare-styleable>
说明一下这些属性都是做什么用的,首先看一下加载中的状态:
加载中会有两个自定义属性,loadingSrc表示加载中的图片,loadingDescText是图片下面加载中的文本描述,loadingDescText为null或者空字符串的时候就不会显示这部分内容了。
看一下无数据的状态:
无数据有三个属性,图片emptySrc,图片下暂无数据的文本描述emptyDescText,按钮的文本描述emptyActionText,emptyDescText和emptyActionText为null或者空字符串的时候不会显示相应的内容。
然后是加载出错的状态:
同样加载出错也有三个属性,failSrc表示加载失败的图片,failDescText是图片下加载失败的文本描述,failActionText表示按钮重试的文本描述,failDescText和failActionText为null或者空字符串的时候不会显示相应的内容。
还有一个intercept属性表示是否拦截点击事件,设为true时底下的View就无法收到点击事件了。
完整的代码如下:
class LoadingLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
companion object {
private const val STATE_LOADING = 0 //加载中
private const val STATE_EMPTY = 1 //无数据
private const val STATE_FAIL = 2 //加载失败
private const val STATE_HIDE = 3 //隐藏
}
var loadingSrc: Drawable? = null //加载中的图片
set(value) {
field = value
mLoadingIv.setImageDrawable(value)
}
var loadingDescText: String = "" //加载中的文本描述
set(value) {
field = value
setDescText(mDescTv, value)
}
var emptySrc: Drawable? = null //无数据的图片
set(value) {
field = value
mEmptyIv.setImageDrawable(value)
}
var emptyDescText: String = "" //无数据的文本描述
set(value) {
field = value
setDescText(mDescTv, value)
}
var emptyActionText: String = "" //无数据时操作按钮的文本
set(value) {
field = value
setActionText(mActionBtn, value)
}
var failSrc: Drawable? = null //加载失败的图片
set(value) {
field = value
mFailIv.setImageDrawable(value)
}
var failDescText: String = "" //加载失败的文本描述
set(value) {
field = value
setDescText(mDescTv, value)
}
var failActionText: String = "" //重试的文本描述
set(value) {
field = value
setActionText(mActionBtn, value)
}
private var intercept: Boolean = true //是否拦截点击事件,为true时底下的View无法收到点击事件
//获取根布局,以便设置布局的LayoutParams
fun getRootLayout(): LinearLayout = mRootLayout
private val mRootLayout: LinearLayout
private val mLoadingIv: ImageView
private val mEmptyIv: ImageView
private val mFailIv: ImageView
private val mDescTv: TextView
private val mActionBtn: Button
init {
val layout = LayoutInflater.from(context).inflate(R.layout.layout_loading, this)
with(layout) {
mRootLayout = findViewById(R.id.root_ll)
mLoadingIv = findViewById(R.id.loading_iv)
mEmptyIv = findViewById(R.id.empty_iv)
mFailIv = findViewById(R.id.fail_iv)
mDescTv = findViewById(R.id.desc_tv)
mActionBtn = findViewById(R.id.action_btn)
}
attrs?.let {
val ta = context.obtainStyledAttributes(it, R.styleable.LoadingLayout)
loadingSrc = ta.getDrawable(R.styleable.LoadingLayout_loadingSrc) ?: resources.getDrawable(R.drawable.ic_load_loading)
loadingDescText = ta.getString(R.styleable.LoadingLayout_loadingDescText) ?: ""
emptySrc = ta.getDrawable(R.styleable.LoadingLayout_emptySrc) ?: resources.getDrawable(R.drawable.ic_load_empty)
emptyDescText = ta.getString(R.styleable.LoadingLayout_emptyDescText) ?: ""
emptyActionText = ta.getString(R.styleable.LoadingLayout_emptyActionText) ?: ""
failSrc = ta.getDrawable(R.styleable.LoadingLayout_failSrc) ?: resources.getDrawable(R.drawable.ic_load_fail)
failDescText = ta.getString(R.styleable.LoadingLayout_failDescText) ?: ""
failActionText = ta.getString(R.styleable.LoadingLayout_failActionText) ?: ""
intercept = ta.getBoolean(R.styleable.LoadingLayout_intercept, true)
ta.recycle()
}
setLoadState(STATE_HIDE)
}
//开始加载
fun loadStart() {
setLoadState(STATE_LOADING)
}
//无数据
fun loadEmpty() {
setLoadState(STATE_EMPTY)
}
//加载失败
fun loadFail() {
setLoadState(STATE_FAIL)
}
//加载完成,则隐藏
fun loadComplete() {
setLoadState(STATE_HIDE)
}
private fun setLoadState(loadState: Int) {
if (loadState == STATE_HIDE) {
visibility = View.GONE
} else {
visibility = View.VISIBLE
mLoadingIv.clearAnimation() //取消动画
mLoadingIv.visibility = View.GONE
mEmptyIv.visibility = View.GONE
mFailIv.visibility = View.GONE
mActionBtn.visibility = View.GONE
when (loadState) {
STATE_LOADING -> {
mLoadingIv.visibility = View.VISIBLE
mLoadingIv.setImageDrawable(loadingSrc)
val animation = RotateAnimation(
0f, 359f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
)
mLoadingIv.startAnimation(animation.apply {
duration = 800
repeatCount = Animation.INFINITE
repeatMode = Animation.RESTART
interpolator = AccelerateDecelerateInterpolator()
})
setDescText(mDescTv, loadingDescText)
}
STATE_EMPTY -> {
mEmptyIv.visibility = View.VISIBLE
mEmptyIv.setImageDrawable(emptySrc)
setDescText(mDescTv, emptyDescText)
setActionText(mActionBtn, emptyActionText)
mActionBtn.setOnClickListener { mOnEmptyListener?.invoke() }
}
STATE_FAIL -> {
mFailIv.visibility = View.VISIBLE
mFailIv.setImageDrawable(failSrc)
setDescText(mDescTv, failDescText)
setActionText(mActionBtn, failActionText)
mActionBtn.setOnClickListener { mOnFailListener?.invoke() }
}
}
}
}
private fun setDescText(tv: TextView, desc: String) {
if (TextUtils.isEmpty(desc)) {
tv.visibility = View.GONE
} else {
tv.visibility = View.VISIBLE
tv.text = desc
}
}
private fun setActionText(btn: Button, text: String) {
if (TextUtils.isEmpty(text)) {
btn.visibility = View.GONE
} else {
btn.visibility = View.VISIBLE
btn.text = text
}
}
private var mOnEmptyListener: (() -> Unit)? = null
//无数据时相应的操作
fun setOnEmptyListener(listener: () -> Unit) {
mOnEmptyListener = listener
}
private var mOnFailListener: (() -> Unit)? = null
//加载失败时相应的操作,如点击重试
fun setOnFailListener(listener: () -> Unit) {
mOnFailListener = listener
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
return intercept //拦截点击事件
}
}
?layout_loading布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tool="http://schemas.android.com/tools"
android:id="@+id/root_ll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/loading_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tool:src="@drawable/ic_load_loading" />
<ImageView
android:id="@+id/empty_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tool:src="@drawable/ic_load_empty" />
<ImageView
android:id="@+id/fail_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tool:src="@drawable/ic_load_fail" />
<TextView
android:id="@+id/desc_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginTop="10dp"
android:layout_marginRight="30dp"
android:includeFontPadding="false"
android:textColor="#a3a3a3"
android:textSize="12sp"
tool:text="网络不给力,请刷新重试" />
<Button
android:id="@+id/action_btn"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_marginTop="20dp"
android:background="@drawable/shape_loading_btn_bg"
android:includeFontPadding="false"
android:paddingLeft="38dp"
android:paddingRight="38dp"
android:textColor="@color/black"
android:textSize="16sp"
tool:text="刷新" />
</LinearLayout>
使用如下:
在xml布局中:
<com.android.widget.LoadingLayout
android:id="@+id/loading_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:emptyActionText="去看看"
app:emptyDescText="暂无数据"
app:failActionText="点击重试"
app:failDescText="网络不给力,请刷新重试"
app:loadingDescText="加载中..." />
?在代码中使用:
loadingLayout.loadStart() //开始加载
loadingLayout.loadComplete() //加载完成,隐藏控件
loadingLayout.loadFail() //加载失败
loadingLayout.loadEmpty() //无数据
loadingLayout.setOnEmptyListener { } //无数据时点击按钮回调
loadingLayout.setOnFailListener { } //加载失败时点击按钮回调
?源码地址
|