該功能是支援單獨為某個佈局設定狀態改變的,比如很多同學提到的我一個listview的資料沒有獲取到,fun initPage(targetView: Any),這個targetView你只需要設定成你的listview或者包裹你listview的parent佈局就OK了,具體原理可以看下面的程式碼解析啊,遍歷獲取索引,然後記錄索引值....
專案中我們經常會用到的載入資料,載入完資料後顯示內容,如果沒有資料顯示一個空白頁,這是如果網路錯誤了顯示一個網路錯誤頁,自定義一個PageLayout。
DownLoad
緒論
Android中經常使用一個空白頁和網路錯誤頁用來提高使用者體驗,給使用者一個較好的感官,如果獲取到的資料為空,那麼會顯示一個空白資料頁,如果在獲取資料的過程中網路錯誤了,會顯示一個網路異常頁,像最近比較火的某東這樣,見下圖。網上也有一些開源的元件,大部分都是自定義繼承某個佈局在xml中讓其作為跟佈局,然後將自己的內容佈局新增進去,效果也都不錯,但是個人總覺得稍微有些麻煩,不是那麼靈活,n多個xml佈局都去定義,寫的心煩,所以有了今天的主角。
思考
實現的思路實際上是和上面說的一樣,只不過換了一種方式,我們手動獲取到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()做了些什麼呢?我們先看一張圖:
一個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中未使用,自行解決
效果圖
程式碼已經上傳到Githubgithub.com/Hankkin/Pag…
Reading:一款不錯的Material Desgin風格的Kotlin版本的開源APP github.com/Hankkin/Rea…
歡迎大家Follow、star、fork,謝謝 如果有不合適的地方,請提issues討論指正