我对空数据页面等公共页面实现的一些想法

1,898 阅读6分钟

我对空数据页面等公共页面实现的一些想法

在很久很久以前,我看到一篇文章如题:对空数据页面等公共页面实现的一些思考。当时觉得作者的想法很奇妙,于是在一个项目上开始使用,但是在使用的过程中遇到一个问题,即在Fragment的时候使用起来会将底部的BottomBar全部一起遮罩。后来在另一个项目的时候不得不使用了另一个satr数很多的LoadSir,但是使用过程中,始终觉得需要配置的地方太麻烦了,后来就构思能不能自己根据第一篇文章的思路写一个空布局的库来使用?于是就有了下面的尝试!

首先,在Activity的布局上面,空布局不需要考虑View或者RecyclerView这种列表,前者的话使用ViewSub或者其它方式都可以解决,使用空布局的话感觉有种杀鸡用牛刀的感觉。毕竟因为替换一个View就添加一个WindowManager有点得不偿失,而且View替换的频率一般情况下比替换整个布局要多,因此我觉得这种情况下不必要在框架支持,而是用户自己处理更合适。而RecyclerView的空布局这些公共页面就更简单了,很多Adapter都支持了设置空页面,比如鸿洋的CommonAdapter通过简单的装饰者模式就解决了这个问题,因此在一开始我们就排除掉这些小问题,接下来就开始真正的干货了!

1、Activity的公共布局

这个地方基本上是完全照搬了对空数据页面等公共页面实现的一些思考里面的思路,唯一区别是将id进行了设置,然后在对空布局重置以后销毁了重试按钮的点击事件,具体代码如下:

**
 * 创建日期:2019/3/28 0028on 上午 9:48
 * 描述:空数据等页面布局
 * @author:Vincent
 * QQ:3332168769
 * 备注:
 */
@SuppressLint("StaticFieldLeak")
object SpaceLayout {

    private lateinit var emptyLayout: View
    private lateinit var loadingLayout: View
    private lateinit var networkErrorLayout: View
    private var currentLayout: View? = null
    private lateinit var mContext: Context
    private var isAresShowing = false
    private var onRetryClickedListener: OnRetryClickedListener? = null
    private var retryId = 0


    /**
     * 初始化
     */
    fun init(context: Context) {
        mContext = context
    }

    /**
     * 设置空数据界面的布局
     */
    fun setEmptyLayout(resId: Int) {
        emptyLayout = getLayout(resId)
        emptyLayout.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
    }

    /**
     * 设置加载中界面的布局
     */
    fun setLoadingLayout(resId: Int) {
        loadingLayout = getLayout(resId)
        loadingLayout.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
    }

    /**
     * 设置网络错误界面的布局
     */
    fun setNetworkErrorLayout(resId: Int) {
        networkErrorLayout = getLayout(resId)
        networkErrorLayout.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)

    }

    /**
     * 展示空数据界面
     * target的大小及位置决定了window界面在实际屏幕中的展示大小及位置
     */
    fun showEmptyLayout(target: View, wm: WindowManager) {
        if (currentLayout != null) {
            wm.removeView(currentLayout)
        }
        isAresShowing = true
        currentLayout = emptyLayout
        wm.addView(currentLayout, setLayoutParams(target))
    }


    /**
     * 展示加载中界面
     * target的大小及位置决定了window界面在实际屏幕中的展示大小及位置
     */
    fun showLoadingLayout(target: View, wm: WindowManager) {
        if (currentLayout != null) {
            wm.removeView(currentLayout)
        }
        isAresShowing = true
        currentLayout = loadingLayout
        wm.addView(currentLayout, setLayoutParams(target))
    }


    /**
     * 展示网络错误界面
     * target的大小及位置决定了window界面在实际屏幕中的展示大小及位置
     */
    fun showNetworkErrorLayout(target: View, wm: WindowManager) {
        if (currentLayout != null) {
            wm.removeView(currentLayout)
        }
        isAresShowing = true
        onRetryClickedListener?.let { listener ->
            networkErrorLayout.findViewById<View>(retryId).setOnClickListener {
                listener.onRetryClick()
            }
        }

        currentLayout = networkErrorLayout
        wm.addView(currentLayout, setLayoutParams(target))
    }

   




    private fun setLayoutParams(target: View): WindowManager.LayoutParams {
        val wlp = WindowManager.LayoutParams()
        wlp.format = PixelFormat.TRANSPARENT
        wlp.flags = (WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
        val location = IntArray(2)
        target.getLocationOnScreen(location)
        wlp.x = location[0]
        wlp.y = location[1]
        wlp.height = target.height
        wlp.width = target.width
        wlp.type = WindowManager.LayoutParams.FIRST_SUB_WINDOW
        wlp.gravity = Gravity.START or Gravity.TOP
        return wlp
    }

    private fun getLayout(resId: Int): ViewGroup {
        val inflater = mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        return inflater.inflate(resId, null) as ViewGroup
    }

    interface OnRetryClickedListener {
        fun onRetryClick()
    }

    fun setOnRetryClickedListener(id: Int, listener: OnRetryClickedListener) {
        retryId = id
        onRetryClickedListener = listener
    }

    fun onDestroy(wm: WindowManager) {
        isAresShowing = false
        currentLayout?.let {
            wm.removeView(currentLayout)
            currentLayout = null
        }
        // 重置 防止在不同的页面调用相同回调事件
        retryId = 0
        onRetryClickedListener = null
    }
}

使用方式也是一模一样:

  // 如果首页不加载网络数据 建议在此处初始化,有利于提高app启动速度
        SpaceLayout.init(this)
        SpaceLayout.setEmptyLayout(R.layout.layout_empty)
        SpaceLayout.setLoadingLayout(R.layout.layout_loading)
        SpaceLayout.setNetworkErrorLayout(R.layout.network_error2_layout)
        
        
    // 重置公共布局
        tv_rightMenu.setOnClickListener {
            SpaceLayout.onDestroy(windowManager)
        }
        // 展示空布局
        ll_btn_empty.setOnClickListener {
            SpaceLayout.showEmptyLayout(ll_content, windowManager)
        }
        // 展示加载中布局
        ll_btn_loading.setOnClickListener {
            SpaceLayout.showLoadingLayout(ll_content, windowManager)
        }
        
        // 展示网络异常页面,支持点击事件回调
        ll_btn_error.setOnClickListener {
            // 防止回调事件错乱 因此回调事件的作用只有一次 即重置的时候对事件进行了回收
            SpaceLayout.setOnRetryClickedListener(R.id.retry,object :SpaceLayout.OnRetryClickedListener{
                override fun onRetryClick() {
                    SpaceLayout.onDestroy(windowManager)
                }

            })
            SpaceLayout.showNetworkErrorLayout(ll_content, windowManager)
        }

2、Fragment公共布局

fragment公共布局为什么和Activity不一样呢?上面说了一个问题,使用的时候对Bottombar一起遮罩,然后还有另一个问题:当Fragment的空布局显示的时候,切换到其它Fragment空布局的显示与关闭也是一个麻烦的事情,虽然可以通过标志位来开启与关闭,但是操作上不是增加麻烦了吗? 为了解决这个问题,我想到了通过对Fragment增加一个子Fragment来覆盖整个Fragment,代码如下:

嵌套Fragment

但是使用的时候却发现嵌套的Fragment无法覆盖原有布局,只能在原布局下面显示,不管是使用add还是replace都没有办法,增加背景色依然无法解决。

嵌套Fragment效果

后来有人建议像前端那样设置Fragment的层级(index),但是我没有找到相关属性,只能无奈放弃这个思路。 后来查看了LoadSir里面Fragment的使用,发现也是增加了一个布局嵌套Fragment根布局。虽然知道这样会增加Fragment布局的层数,但是为了实现这个效果也只有牺牲这个层数了(之前看到过某篇文章分析,当布局层数大于4层才会对性能有影响),但是我们可以做的是只对每一个需要增加一个公共布局的Fragment动态设置,这样即可避免不需要的Fragment增加布局层数,也方便使用,代码如下:

/**
     * 展示空数据界面
     *
     */
    fun showEmptyLayout(target: Fragment, empty: View = emptyLayout) {
        showFragmentLayout(false, target, empty)
    }

    /**
     * 展示加载中界面
     *
     */
    fun showLoadingLayout(target: Fragment, empty: View = loadingLayout) {
        showFragmentLayout(false, target, empty)
    }

    /**
     * 展示网络错误界面
     *
     */
    fun showNetworkErrorLayout(
        target: Fragment,
        empty: View = networkErrorLayout,
        id: Int = 0,
        listener: OnRetryClickedListener? = null
    ) {
        if (id != 0) {
            setOnFragmentRetryClickedListener(target, id, listener)

        }
        showFragmentLayout(true, target, empty)
    }

    fun setOnFragmentRetryClickedListener(target: Fragment, id: Int = 0, listener: OnRetryClickedListener? = null) {
        if (target.view!! !is LoadLayout) {
            throw RuntimeException("请在 onCreateView 方法处将根View替换为 LoadLayout")
        }
        val loadLayout = target.view as LoadLayout
        loadLayout.setListener(id, listener)
    }

    /**
     * 重置 Fragment 状态
     */
    fun onDestroy(target: Fragment) {
        if (target.view!! !is LoadLayout) {
            throw RuntimeException("请在 onCreateView 方法处将根View替换为 LoadLayout")
        }
        val loadLayout = target.view as LoadLayout
        loadLayout.restView()
    }

    /**
     * Fragment 显示状态View
     * fragment Root View 必须设置 id
     */
    private fun showFragmentLayout(isRetry: Boolean, target: Fragment, empty: View) {
        if (target.view!! !is LoadLayout) {
            throw RuntimeException("请在 onCreateView 方法处将根View替换为 LoadLayout")
        }
        val loadLayout = target.view as LoadLayout
        if (isRetry) {
            loadLayout.showNetworkErrorLayout(empty)
        } else {
            loadLayout.showView(empty)
        }


    }


    @SuppressLint("ViewConstructor")
    class LoadLayout(private val mView: View) : FrameLayout(mView.context) {
        private var retryId = 0
        private var mListener: OnRetryClickedListener? = null

        init {
            addView(mView)
        }

        fun setListener(id: Int, listener: OnRetryClickedListener?) {
            this.retryId = id
            this.mListener = listener
        }

        fun showNetworkErrorLayout(spaceView: View) {
            if (retryId != 0) {
                spaceView.findViewById<View>(retryId).setOnClickListener {
                    mListener?.onRetryClick()
                }
            }
            showView(spaceView)
        }

        fun showView(spaceView: View) {
            mView.visibility = View.GONE
            if (childCount > 1) {
                removeViewAt(1)
            }
            addView(spaceView, 1)
        }

        fun restView() {
            mView.visibility = View.VISIBLE
            if (childCount > 1) {
                removeViewAt(1)
            }
        }

    }

效果如下:

空布局效果图


以上就是我对公共页面的一些总结,希望嵌套Fragment的地方有大佬能指点一下解决思路,谢谢!

源码

示例