Android实现加载状态控件LoadingLayout

通常我们浏览新闻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 { }   //加载失败时点击按钮回调

 源码地址

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值