在好久好久之前,我看到一篇文章如题:对空数据页面等公共页面实现的一些思考。当时以为做者的想法很奇妙,因而在一个项目上开始使用,可是在使用的过程当中遇到一个问题,即在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
源码布局