【PageLayout】非常簡單的一鍵切換載入-空資料-錯誤頁,支援自定義

Hankkin發表於2018-09-09

該功能是支援單獨為某個佈局設定狀態改變的,比如很多同學提到的我一個listview的資料沒有獲取到,fun initPage(targetView: Any),這個targetView你只需要設定成你的listview或者包裹你listview的parent佈局就OK了,具體原理可以看下面的程式碼解析啊,遍歷獲取索引,然後記錄索引值....

專案中我們經常會用到的載入資料,載入完資料後顯示內容,如果沒有資料顯示一個空白頁,這是如果網路錯誤了顯示一個網路錯誤頁,自定義一個PageLayout。

DownLoad

fir.im/pagelayout

【PageLayout】非常簡單的一鍵切換載入-空資料-錯誤頁,支援自定義

緒論

Android中經常使用一個空白頁和網路錯誤頁用來提高使用者體驗,給使用者一個較好的感官,如果獲取到的資料為空,那麼會顯示一個空白資料頁,如果在獲取資料的過程中網路錯誤了,會顯示一個網路異常頁,像最近比較火的某東這樣,見下圖。網上也有一些開源的元件,大部分都是自定義繼承某個佈局在xml中讓其作為跟佈局,然後將自己的內容佈局新增進去,效果也都不錯,但是個人總覺得稍微有些麻煩,不是那麼靈活,n多個xml佈局都去定義,寫的心煩,所以有了今天的主角。

【PageLayout】非常簡單的一鍵切換載入-空資料-錯誤頁,支援自定義 【PageLayout】非常簡單的一鍵切換載入-空資料-錯誤頁,支援自定義

思考

實現的思路實際上是和上面說的一樣,只不過換了一種方式,我們手動獲取到contentView,將它從DecorView中移除,然後交給PageLayout取管理。當時考慮的時候就是不想在每個xml中去寫頁面切換的佈局,那麼我們可不可以用Java程式碼去控制?帶著下面幾個問題一起來看一下。

  • 1.自定義一個佈局讓其作為跟佈局
  • 2.提供切換載入loading空白頁empty錯誤頁errror內容頁content功能
  • 3.怎麼讓其取管理上邊的四個頁面?
  • 4.contentView怎麼新增?
  • 5.如果我想切換的跟佈局不是個Activity或者Fragment怎麼辦?
  • 6.因為切換頁面狀態的功能一般都是一個APP統一的,那麼可不可以一鍵配置呢?

實現

1.程式碼設計

首先我們定義PageLayout繼承FrameLayout或者LinearLayou或者其他的佈局都可以,然後我們需要提供切換四個佈局的功能,當然如果支援自定義就更好了,還有狀態佈局裡面的一些屬性,還方便一鍵配置,所以最後採用了Builder模式來建立,使用方式就和Android裡面的AlertDialog一樣,通過Builder去構建一個PageLayout。最後的樣子是長這樣的:

預設樣式

PageLayout.Builder(this)
                .initPage(ll_default)
                .setOnRetryListener(object : PageLayout.OnRetryClickListener{
                    override fun onRetry() {
                        loadData()
                    }

                })
                .create()
複製程式碼

自定義樣式

PageLayout.Builder(this)
                .initPage(ll_demo)
                .setLoading(R.layout.layout_loading_demo)
                .setEmpty(R.layout.layout_empty_demo)
                .setError(R.layout.layout_error_demo,R.id.tv_page_error_demo,object : PageLayout.OnRetryClickListener{
                    override fun onRetry() {
                        loadData()
                    }
                })
                .setEmptyDrawable(R.drawable.pic_empty)
                .setErrorDrawable(R.drawable.pic_error)
                .create()
複製程式碼

2.設定PageLayout

考慮好了程式碼設計方式之後,我們來具體實現功能,首先需要考慮上面的5,6點:

contentView怎麼新增?

如果我想切換的跟佈局不是個Activity或者Fragment怎麼辦?

1.Activity

如果我們要切換的跟佈局是個Activity時,首先我們需要了解一下Android中的setContentView()方法,很熟悉,是我們新建完Activity後預設會在生命週期方法onCreate()中預設存在的,那麼setContentView()做了些什麼呢?我們先看一張圖:

image

一個Activity是通過ActivityThread建立出來的,建立完畢後,會將DecorView新增到Window中,同時會建立ViewRootImpl物件,並將ViewRootImpl物件和DecorView建立關聯,setContentView()是通過getWindow()呼叫的,這裡的window實際初始化的時候初始化為PhoneWindow,也就是說Activity會呼叫PhoneWindow的setContentView()將layout佈局新增到DecorView上,而此時的DecorView就是那個最底層的View。然後通過LayoutInflater.infalte()方法載入佈局生成View物件並通過addView()方法新增到Window上,(一層一層的疊加到Window上)所以,Activity其實不是顯示檢視,Window才是真正的顯示檢視。

再來看上面的那張圖,可以說DecorView是一個介面的真正跟佈局,TitleView我們可以通過設定theme樣式顯示隱藏的,狀態佈局切換時我們不考慮TitleView,我們只需要考慮ContentView,而ContentView也就是android.R.id.content,知道了這些我們來看看怎麼獲取將contenView交給PageLayout管理。

2.Fragment、View

如果我們要切換的跟佈局是個Fragment、View時,我們只需要獲取到它的parent

該功能是支援單獨為某個佈局設定狀態改變的,比如很多同學提到的我一個listview的資料沒有獲取到,fun initPage(targetView: Any),這個targetView你只需要設定成你的listview或者包裹你listview的parent佈局就OK了,具體原理可以看下面的程式碼解析啊,遍歷獲取索引,然後記錄索引值....

3.PageLayout設定跟佈局

獲取到了contentView跟佈局後,我們要移除自己的顯示內容的佈局,並把這個佈局交給PageLayout,下面看一下程式碼,註釋的很詳細了

 /**
         * set target view for root
         */
        fun initPage(targetView: Any): Builder {
            var content: ViewGroup? = null
            when (targetView) {
                is Activity -> {    //如果是Activity,獲取到android.R.content
                    mContext = targetView
                    content = (mContext as Activity).findViewById(android.R.id.content)
                }
                is Fragment -> {    //如果是Fragment獲取到parent
                    mContext = targetView.activity!!
                    content = (targetView.view)?.parent as ViewGroup
                }
                is View -> {        //如果是View,也取到parent
                    mContext = targetView.context
                    try {
                        content = (targetView.parent) as ViewGroup
                    } catch (e: TypeCastException) {
                    }
                }
            }
            val childCount = content?.childCount
            var index = 0
            val oldContent: View
            if (targetView is View) {   //如果是某個線性佈局或者相對佈局時,遍歷它的孩子,找到對應的索引,記錄下來
                oldContent = targetView
                childCount?.let {
                    for (i in 0 until childCount) {
                        if (content!!.getChildAt(i) === oldContent) {
                            index = i
                            break
                        }
                    }
                }

            } else {    //如果是Activity或者Fragment時,取到索引為第一個的View
                oldContent = content!!.getChildAt(0)
            }
            mPageLayout.mContent = oldContent   //給PageLayout設定contentView
            mPageLayout.removeAllViews()    
            content?.removeView(oldContent)     //將本身content移除,並且把PageLayout新增到DecorView中去
            val lp = oldContent.layoutParams
            content?.addView(mPageLayout, index, lp)
            mPageLayout.addView(oldContent)
            initDefault()   //設定預設狀態佈局
            return this
        }
複製程式碼

這樣我們就解決了上面的5,6的問題。

4.其他

  • 因為錯誤佈局中一般都包括一個點選重試的功能,如果你需要自定義佈局,你可以在配置PageLayout之前,設定好錯誤佈局和點選事件,然後setError進去,同時也提供了一個預設方式的方法
fun setError(errorView: Int, errorClickId: Int, onRetryClickListener: OnRetryClickListener)
複製程式碼
  • 考慮到此功能的APP統一性,所以並沒有提供過多的自定義功能,如果你需要的話,你都可以提前設定好View,然後進行set
  • 之前和同事討論,xml形式和程式碼形式哪個更方便更靈活,這些都屬於個人喜好吧,如果你更喜歡在xml裡寫的話,你可以進行改造,也挺簡單,目前沒提供xml方式,PageLayout的初衷就是模仿AlertDialog方式,隨時隨地使用狀態佈局切換
  • 你也可以在BaseActivity和BaseFragment中進行PageLayout的初始化,Demo中未使用,自行解決

效果圖

image

程式碼已經上傳到Githubgithub.com/Hankkin/Pag…

Reading:一款不錯的Material Desgin風格的Kotlin版本的開源APP github.com/Hankkin/Rea…

歡迎大家Follow、star、fork,謝謝 如果有不合適的地方,請提issues討論指正

相關文章