我對空資料頁面等公共頁面實現的一些想法

Vincent_9920發表於2019-04-03

我對空資料頁面等公共頁面實現的一些想法

在很久很久以前,我看到一篇文章如題:對空資料頁面等公共頁面實現的一些思考。當時覺得作者的想法很奇妙,於是在一個專案上開始使用,但是在使用的過程中遇到一個問題,即在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的地方有大佬能指點一下解決思路,謝謝!

原始碼

示例

相關文章