在好久好久之前,我看到一篇文章如題:對空數據頁面等公共頁面實現的一些思考。當時以爲做者的想法很奇妙,因而在一個項目上開始使用,可是在使用的過程當中遇到一個問題,即在Fragment的時候使用起來會將底部的BottomBar所有一塊兒遮罩。後來在另外一個項目的時候不得不使用了另外一個satr數不少的LoadSir,可是使用過程當中,始終以爲須要配置的地方太麻煩了,後來就構思能不能本身根據第一篇文章的思路寫一個空佈局的庫來使用?因而就有了下面的嘗試!前端
首先,在Activity的佈局上面,空佈局不須要考慮View或者RecyclerView這種列表,前者的話使用ViewSub或者其它方式均可以解決,使用空佈局的話感受有種殺雞用牛刀的感受。畢竟由於替換一個View就添加一個WindowManager有點得不償失,並且View替換的頻率通常狀況下比替換整個佈局要多,所以我以爲這種狀況下沒必要要在框架支持,而是用戶本身處理更合適。而RecyclerView的空佈局這些公共頁面就更簡單了,不少Adapter都支持了設置空頁面,好比鴻洋的CommonAdapter經過簡單的裝飾者模式就解決了這個問題,所以在一開始咱們就排除掉這些小問題,接下來就開始真正的乾貨了!java
這個地方基本上是徹底照搬了對空數據頁面等公共頁面實現的一些思考裏面的思路,惟一區別是將id進行了設置,而後在對空佈局重置之後銷燬了重試按鈕的點擊事件,具體代碼以下:git
**
* 建立日期: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
}
}
複製代碼
使用方式也是如出一轍:github
// 若是首頁不加載網絡數據 建議在此處初始化,有利於提升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)
}
複製代碼
fragment公共佈局爲何和Activity不同呢?上面說了一個問題,使用的時候對Bottombar一塊兒遮罩,而後還有另外一個問題:當Fragment的空佈局顯示的時候,切換到其它Fragment空佈局的顯示與關閉也是一個麻煩的事情,雖然能夠經過標誌位來開啓與關閉,可是操做上不是增長麻煩了嗎? 爲了解決這個問題,我想到了經過對Fragment增長一個子Fragment來覆蓋整個Fragment,代碼以下:bash
可是使用的時候卻發現嵌套的Fragment沒法覆蓋原有佈局,只能在原佈局下面顯示,不論是使用add仍是replace都沒有辦法,增長背景色依然沒法解決。網絡
後來有人建議像前端那樣設置Fragment的層級(index),可是我沒有找到相關屬性,只能無奈放棄這個思路。 後來查看了LoadSir裏面Fragment的使用,發現也是增長了一個佈局嵌套Fragment根佈局。雖然知道這樣會增長Fragment佈局的層數,可是爲了實現這個效果也只有犧牲這個層數了(以前看到過某篇文章分析,當佈局層數大於4層纔會對性能有影響),可是咱們能夠作的是隻對每個須要增長一個公共佈局的Fragment動態設置,這樣便可避免不須要的Fragment增長佈局層數,也方便使用,代碼以下:app
/**
* 展現空數據界面
*
*/
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的地方有大佬能指點一下解決思路,謝謝!ide
源碼佈局