作者: Jooyer, 時間: 2018.08.08
Github地址,歡迎點贊,fork其實一般的APP 都是這麼幾個狀態,本次效果和其他開源沒有什麼區別,只是在方法數量和類數量上,和我之前部落格一樣,比較少! 這是自我吹捧,哈哈!
只需要三個類 + 幾個 XML 檔案即可,即拷即用
接下來我們依次講解:
- StatusManager
- RootStatusLayout
- OnRetryListener
- 其他幾個 XML 檔案
首先我們看看 StatusManager:
/**
* Desc: 檢視管理器
* Author: Jooyer
* Date: 2018-07-30
* Time: 11:46
*/
class StatusManager(builder: Builder) {
var mContext: Context
var mNetworkErrorVs: ViewStub? = null
var mNetworkErrorView:View? = null
var mNetWorkErrorRetryViewId: Int = 0
var mEmptyDataVs: ViewStub? = null
var mEmptyDataView:View? = null
var mEmptyDataRetryViewId: Int = 0
var mErrorVs: ViewStub? = null
var mErrorView:View? = null
var mErrorRetryViewId: Int = 0
var mLoadingLayoutResId: Int = 0
var mContentLayoutResId: Int = 0
var mRetryViewId: Int = 0
var mContentLayoutView: View? = null
var mRootFrameLayout: RootStatusLayout? = null
/**
* 顯示loading
*/
fun showLoading() {
mRootFrameLayout?.showLoading()
}
/**
* 顯示內容
*/
fun showContent() {
mRootFrameLayout?.showContent()
}
/**
* 顯示空資料
*/
fun showEmptyData() {
mRootFrameLayout?.showEmptyData()
}
/**
* 顯示網路異常
*/
fun showNetWorkError() {
mRootFrameLayout?.showNetWorkError()
}
/**
* 顯示異常
*/
fun showError() {
mRootFrameLayout?.showError()
}
/**
* 得到root 佈局
*/
fun getRootLayout(): View {
return mRootFrameLayout!!
}
class Builder(val context: Context) {
var loadingLayoutResId: Int = 0
var contentLayoutResId: Int = 0
var contentLayoutView: View? = null
var netWorkErrorVs: ViewStub? = null
var netWorkErrorRetryViewId: Int = 0
var emptyDataVs: ViewStub? = null
var emptyDataRetryViewId: Int = 0
var errorVs: ViewStub? = null
var errorRetryViewId: Int = 0
var retryViewId: Int = 0
// var onShowHideViewListener: OnShowOrHideViewListener? = null
var onRetryListener: OnRetryListener? = null
fun loadingView(@LayoutRes loadingLayoutResId: Int): Builder {
this.loadingLayoutResId = loadingLayoutResId
return this
}
fun netWorkErrorView(@LayoutRes newWorkErrorId: Int): Builder {
netWorkErrorVs = ViewStub(context)
netWorkErrorVs!!.layoutResource = newWorkErrorId
return this
}
fun emptyDataView(@LayoutRes noDataViewId: Int): Builder {
emptyDataVs = ViewStub(context)
emptyDataVs!!.layoutResource = noDataViewId
return this
}
fun errorView(@LayoutRes errorViewId: Int): Builder {
errorVs = ViewStub(context)
errorVs!!.layoutResource = errorViewId
return this
}
fun contentView(contentLayoutView: View): Builder {
this.contentLayoutView = contentLayoutView
return this
}
fun contentViewResId(@LayoutRes contentLayoutResId: Int): Builder {
this.contentLayoutResId = contentLayoutResId
return this
}
fun netWorkErrorRetryViewId(netWorkErrorRetryViewId: Int): Builder {
this.netWorkErrorRetryViewId = netWorkErrorRetryViewId
return this
}
fun emptyDataRetryViewId(emptyDataRetryViewId: Int): Builder {
this.emptyDataRetryViewId = emptyDataRetryViewId
return this
}
fun errorRetryViewId(errorRetryViewId: Int): Builder {
this.errorRetryViewId = errorRetryViewId
return this
}
fun retryViewId(retryViewId: Int): Builder {
this.retryViewId = retryViewId
return this
}
fun onRetryListener(onRetryListener: OnRetryListener): Builder {
this.onRetryListener = onRetryListener
return this
}
fun build(): StatusManager {
return StatusManager(this)
}
}
companion object {
fun newBuilder(context: Context): Builder {
return Builder(context)
}
}
init {
mContext = builder.context
mLoadingLayoutResId = builder.loadingLayoutResId
mNetworkErrorVs = builder.netWorkErrorVs
mNetWorkErrorRetryViewId = builder.netWorkErrorRetryViewId
mEmptyDataVs = builder.emptyDataVs
mEmptyDataRetryViewId = builder.emptyDataRetryViewId
mErrorVs = builder.errorVs
mErrorRetryViewId = builder.errorRetryViewId
mContentLayoutResId = builder.contentLayoutResId
mRetryViewId = builder.retryViewId
mContentLayoutView = builder.contentLayoutView
mRootFrameLayout = RootStatusLayout(mContext)
mRootFrameLayout!!.setStatusManager(this)
mRootFrameLayout!!.setOnRetryListener(builder.onRetryListener)
}
}
複製程式碼
使用建造者模式, 初始化必要的佈局資訊和檢視控制元件,同時使用 ViewStub 優化異常View
然後我們看看 RootStatusLayout :
/**
* Desc: 檢視管理佈局控制元件
* Author: Jooyer
* Date: 2018-07-30
* Time: 11:23
*/
class RootStatusLayout(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
: ConstraintLayout(context, attrs, defStyleAttr) {
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?) : this(context, null, 0)
/**
* loading 載入id
*/
val LAYOUT_LOADING_ID = 1
/**
* 內容id
*/
val LAYOUT_CONTENT_ID = 2
/**
* 異常id
*/
val LAYOUT_ERROR_ID = 3
/**
* 網路異常id
*/
val LAYOUT_NETWORK_ERROR_ID = 4
/**
* 空資料id
*/
val LAYOUT_EMPTY_ID = 5
/**
* 存放佈局集合
*/
private val mLayoutViews = SparseArray<View>()
/**
* 檢視管理器
*/
private var mStatusLayoutManager: StatusManager? = null
/**
* 不同檢視的切換
*/
// private var onShowHideViewListener: OnShowOrHideViewListener? = null
/**
* 點選重試按鈕回撥
*/
private var onRetryListener: OnRetryListener? = null
fun setStatusManager(manager: StatusManager) {
mStatusLayoutManager = manager
addAllLayoutViewsToRoot()
}
private fun addAllLayoutViewsToRoot() {
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT)
if (0 != mStatusLayoutManager?.mContentLayoutResId) {
addLayoutResId(mStatusLayoutManager?.mContentLayoutResId!!, LAYOUT_CONTENT_ID, params)
} else if (null != mStatusLayoutManager?.mContentLayoutView) {
addLayoutView(mStatusLayoutManager?.mContentLayoutView!!, LAYOUT_CONTENT_ID, params)
}
if (0 != mStatusLayoutManager?.mLoadingLayoutResId) {
addLayoutResId(mStatusLayoutManager?.mLoadingLayoutResId!!, LAYOUT_LOADING_ID, params)
}
if (null != mStatusLayoutManager?.mEmptyDataVs) {
addView(mStatusLayoutManager?.mEmptyDataVs, params)
}
if (null != mStatusLayoutManager?.mErrorVs) {
addView(mStatusLayoutManager?.mErrorVs, params)
}
if (null != mStatusLayoutManager?.mNetworkErrorVs) {
addView(mStatusLayoutManager?.mNetworkErrorVs, params)
}
}
private fun addLayoutView(layoutView: View, layoutId: Int, param: ViewGroup.LayoutParams) {
mLayoutViews.put(layoutId, layoutView)
addView(layoutView, param)
}
private fun addLayoutResId(@LayoutRes layoutResId: Int, layoutId: Int, param: ViewGroup.LayoutParams) {
val view: View = LayoutInflater.from(context)
.inflate(layoutResId, null)
mLayoutViews.put(layoutId, view)
if (LAYOUT_LOADING_ID == layoutId) {
view.visibility = View.GONE
}
addView(view, param)
}
/**
* 顯示loading
*/
fun showLoading() {
if (mLayoutViews.get(LAYOUT_LOADING_ID) != null)
showHideViewById(LAYOUT_LOADING_ID)
}
/**
* 顯示內容
*/
fun showContent() {
if (mLayoutViews.get(LAYOUT_CONTENT_ID) != null)
showHideViewById(LAYOUT_CONTENT_ID)
}
/**
* 顯示空資料
*/
fun showEmptyData() {
if (inflateLayout(LAYOUT_EMPTY_ID))
showHideViewById(LAYOUT_EMPTY_ID)
}
/**
* 顯示網路異常
*/
fun showNetWorkError() {
if (inflateLayout(LAYOUT_NETWORK_ERROR_ID))
showHideViewById(LAYOUT_NETWORK_ERROR_ID)
}
/**
* 顯示異常
*/
fun showError() {
if (inflateLayout(LAYOUT_ERROR_ID))
showHideViewById(LAYOUT_ERROR_ID)
}
private fun showHideViewById(layoutId: Int) {
for (i in 0 until mLayoutViews.size()) {
val key = mLayoutViews.keyAt(i)
val value = mLayoutViews[key]
// 顯示該 View
if (layoutId == key) {
value.visibility = View.VISIBLE
} else {
if (View.GONE != value.visibility) {
value.visibility = View.GONE
}
}
}
}
fun setOnRetryListener(listener: OnRetryListener?) {
onRetryListener = listener
}
/**
* 載入 StubView
*/
private fun inflateLayout(layoutId: Int): Boolean {
var isShow = true
when (layoutId) {
LAYOUT_NETWORK_ERROR_ID -> {
isShow = when {
null != mStatusLayoutManager?.mNetworkErrorView -> {
retryLoad(mStatusLayoutManager?.mNetworkErrorView!!, mStatusLayoutManager?.mNetWorkErrorRetryViewId!!)
mLayoutViews.put(layoutId, mStatusLayoutManager?.mNetworkErrorView!!)
return true
}
null != mStatusLayoutManager?.mNetworkErrorVs -> {
val view: View = mStatusLayoutManager?.mNetworkErrorVs!!.inflate()
mStatusLayoutManager?.mNetworkErrorView = view
retryLoad(view, mStatusLayoutManager?.mNetWorkErrorRetryViewId!!)
mLayoutViews.put(layoutId, view)
true
}
else -> false
}
}
LAYOUT_ERROR_ID -> {
isShow = when {
null != mStatusLayoutManager?.mErrorView -> {
retryLoad(mStatusLayoutManager?.mErrorView!!, mStatusLayoutManager?.mErrorRetryViewId!!)
mLayoutViews.put(layoutId, mStatusLayoutManager?.mErrorView!!)
return true
}
null != mStatusLayoutManager?.mErrorVs -> {
val view: View = mStatusLayoutManager?.mErrorVs!!.inflate()
mStatusLayoutManager?.mErrorView = view
retryLoad(view, mStatusLayoutManager?.mErrorRetryViewId!!)
mLayoutViews.put(layoutId, view)
true
}
else -> false
}
}
LAYOUT_EMPTY_ID -> {
isShow = when {
null != mStatusLayoutManager?.mEmptyDataView -> {
retryLoad(mStatusLayoutManager?.mEmptyDataView!!, mStatusLayoutManager?.mEmptyDataRetryViewId!!)
mLayoutViews.put(layoutId, mStatusLayoutManager?.mEmptyDataView!!)
return true
}
null != mStatusLayoutManager?.mEmptyDataVs -> {
val view: View = mStatusLayoutManager?.mEmptyDataVs!!.inflate()
mStatusLayoutManager?.mEmptyDataView = view
retryLoad(view, mStatusLayoutManager?.mEmptyDataRetryViewId!!)
mLayoutViews.put(layoutId, view)
true
}
else -> false
}
}
}
return isShow
}
/**
* 載入重試按鈕,並繫結監聽
*/
private fun retryLoad(view: View, layoutResId: Int) {
val retryView: View? = view.findViewById(
if (0 != mStatusLayoutManager?.mRetryViewId!!) {
mStatusLayoutManager?.mRetryViewId!!
} else {
layoutResId
}) ?: return
retryView?.setOnClickListener {
onRetryListener?.onRetry()
}
}
}
複製程式碼
載入必要是檢視到佈局中,並根據需要顯示和隱藏相關 View,邏輯也是很簡單,哈哈!
接著看看最後一個類,其實就是個回撥...
/**
* Desc: 資料異常處理時點選回撥
* Author: Jooyer
* Date: 2018-07-30
* Time: 11:22
*/
interface OnRetryListener{
fun onRetry()
}
複製程式碼
這個也佔了一個類,我喜歡這樣,兩個字 --> 任性
最後就是幾個我就一股腦都丟擲來了,準備接招!!! 你喜歡可以隨意定製,咋舒服咋來.
widget_empty_page.xml -----> 沒有資料時使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="visible">
<ImageView
android:id="@+id/empty_img_status"
android:layout_width="@dimen/width_100"
android:layout_height="@dimen/height_100"
android:background="@android:color/holo_blue_bright"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/padding_10"
android:gravity="center"
android:textSize="@dimen/textSize_16"
android:text="暫時沒有資料! "
/>
</LinearLayout>
複製程式碼
widget_error_page.xml -----> 異常(資料解析異常,伺服器500等) 時使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="visible">
<ImageView
android:id="@+id/error_img_status"
android:layout_width="@dimen/width_100"
android:layout_height="@dimen/height_100"
android:background="@android:color/holo_orange_dark"
/>
<TextView
android:id="@+id/error_text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/padding_10"
android:layout_marginTop="@dimen/padding_10"
android:text="出錯了..."
/>
<TextView
android:id="@+id/tv_retry_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:gravity="center"
android:paddingBottom="@dimen/padding_6"
android:paddingLeft="@dimen/padding_18"
android:paddingRight="@dimen/padding_18"
android:paddingTop="@dimen/padding_6"
android:text="點選重試! "
/>
</LinearLayout>
複製程式碼
widget_nonetwork_page.xml -----> 網路異常時使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="visible">
<ImageView
android:id="@+id/no_network_img_status"
android:layout_width="@dimen/width_100"
android:layout_height="@dimen/height_100"
android:background="@android:color/holo_green_dark"
/>
<TextView
android:id="@+id/no_network_text_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/padding_10"
android:layout_marginTop="@dimen/padding_10"
android:text="網路錯誤!..."
/>
<TextView
android:id="@+id/tv_retry_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:gravity="center"
android:paddingBottom="@dimen/padding_6"
android:paddingLeft="@dimen/padding_18"
android:paddingRight="@dimen/padding_18"
android:paddingTop="@dimen/padding_6"
android:text="點選重試"
/>
</LinearLayout>
複製程式碼
widget_progress_bar.xml ----->載入資料時使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ProgressBar
android:id="@+id/progress_bar_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
/>
</LinearLayout>
複製程式碼
以上就是全部了,如果小夥伴發現有不能執行的,請看下面這個檔案,它是在 values 內哦
dimens.xml -----> 定義了檢視大小
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="width_100">100dp</dimen>
<dimen name="height_100">100dp</dimen>
<dimen name="padding_18">18dp</dimen>
<dimen name="padding_10">10dp</dimen>
<dimen name="padding_6">6dp</dimen>
<dimen name="textSize_16">16sp</dimen>
</resources>
複製程式碼
這次真的是全部了, 下面來介紹用法.
一般我們都需要定義基類,所以本次的檢視管理器也在基類中,請看:
BaseActivity
/**
* Desc: Activity 基類
* Author: Jooyer
* Date: 2018-08-04
* Time: 21:22
*/
abstract class BaseActivity :AppCompatActivity(), OnRetryListener {
/**
* 請求網路異常等介面管理
*/
var mStatusManager: StatusManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (0 != getLayoutId()) {
setContentView(initStatusManager(savedInstanceState))
}
}
/**
* Activity 佈局檔案
*/
abstract fun getLayoutId(): Int
/**
* 初始化 View
*/
abstract fun initializedViews(savedInstanceState: Bundle?, contentView: View)
private fun initStatusManager(savedInstanceState: Bundle?): View {
if (0 != getLayoutId()) {
val contentView = LayoutInflater.from(this)
.inflate(getLayoutId(), null)
initializedViews(savedInstanceState, contentView)
return if (useStatusManager()) {
initialized(contentView)
} else {
contentView.visibility = View.VISIBLE
contentView
}
}
throw IllegalStateException("getLayoutId() 必須呼叫,且返回正常的佈局ID")
}
private fun initialized(contentView: View): View {
mStatusManager = StatusManager.newBuilder(this)
.contentView(contentView)
.loadingView(R.layout.widget_progress_bar)
.emptyDataView(R.layout.widget_empty_page)
.netWorkErrorView(R.layout.widget_nonetwork_page)
.errorView(R.layout.widget_error_page)
.retryViewId(R.id.tv_retry_status) // 注意以上佈局中如果有重試ID,則必須一樣,ID名稱隨意,記得這裡填寫正確
.onRetryListener(this)
.build()
mStatusManager?.showLoading()
return mStatusManager?.getRootLayout()!!
}
/**
* 是否使用檢視佈局管理器,預設不使用
*/
open fun useStatusManager(): Boolean {
return false
}
/**
* 點選檢視中重試按鈕
*/
override fun onRetry() {
Toast.makeText(this,"如果需要點選重試,則重寫 onRetry() 方法",
Toast.LENGTH_SHORT).show()
}
}
複製程式碼
繼續
BaseFragment
/**
* Desc: Fragment 基類
* Author: Jooyer
* Date: 2018-08-04
* Time: 21:23
*/
abstract class BaseFragment: Fragment(), OnRetryListener {
/**
* 請求網路異常等介面管理
*/
var mStatusManager: StatusManager? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return initStatusManager(inflater, container, savedInstanceState)
}
/**
* Fragment 佈局檔案
*/
abstract fun getLayoutId(): Int
abstract fun initializedViews(savedInstanceState: Bundle?, contentView: View)
/**
* 此函式開始資料載入的操作,且僅呼叫一次
* 主要是載入動畫,初始化展示資料的佈局
*/
private fun initStatusManager(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
if (0 != getLayoutId()) {
val contentView = inflater.inflate(getLayoutId(), container, false)
initializedViews(savedInstanceState, contentView)
return if (useStatusManager()) {
initialized(contentView)
} else {
contentView.visibility = View.VISIBLE
contentView
}
}
throw IllegalStateException("getLayoutId() 必須呼叫,且返回正常的佈局ID")
}
private fun initialized(contentView: View): View {
mStatusManager = StatusManager.newBuilder(contentView.context)
.contentView(contentView)
.loadingView(R.layout.widget_progress_bar)
.emptyDataView(R.layout.widget_empty_page)
.netWorkErrorView(R.layout.widget_nonetwork_page)
.errorView(R.layout.widget_error_page)
.retryViewId(R.id.tv_retry_status) // 注意以上佈局中如果有重試ID,則必須一樣,ID名稱隨意,記得這裡填寫正確
.onRetryListener(this)
.build()
mStatusManager?.showLoading()
return mStatusManager?.getRootLayout()!!
}
/**
* 是否使用檢視佈局管理器,預設不使用
*/
open fun useStatusManager(): Boolean {
return false
}
/**
* 點選檢視中重試按鈕
*/
override fun onRetry() {
Toast.makeText(this,"如果需要點選重試,則重寫 onRetry() 方法",
Toast.LENGTH_SHORT).show()
}
}
複製程式碼
PS: onRetry() 的 Toast 僅僅演示用的,具體邏輯,請重寫此方法來處理...
最後看看在 Activity 的用法吧, Fragment 類似,如果小夥伴在 Fragment 中不會使用或者有其他疑問請留言!
class MainActivity : BaseActivity() {
override fun getLayoutId(): Int {
return R.layout.activity_main
}
// 僅僅演示如何通過 findViewById 找到控制元件,其實 Kotlin 不用這麼麻煩
override fun initializedViews(savedInstanceState: Bundle?, contentView: View) {
val tv_main_activity = contentView.findViewById<TextView>(R.id.tv_main_activity)
}
// 如果使用 StateManager 必須重寫下面方法,且返回 true
// 如果返回 true 則會顯示載入的 loading
override fun useStatusManager(): Boolean {
return true
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.state_menu, menu)
return true
}
// 下面展示了顯示各個狀態的方式
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.option_empty_page -> {
mStatusManager?.showEmptyData()
return true
}
R.id.option_error_page -> {
mStatusManager?.showError()
return true
}
R.id.option_loading_page -> {
mStatusManager?.showLoading()
return true
}
R.id.option_network_page -> {
mStatusManager?.showNetWorkError()
return true
}
R.id.option_content_page -> {
mStatusManager?.showContent()
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
// 這個僅僅演示載入 Framgment ,和本例沒有什麼關係
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager.beginTransaction()
.add(R.id.fl_container,MainFragment())
.commit()
}
}
複製程式碼
哈哈,我覺得很簡單啊,小夥伴們覺得呢?雖然簡單不過它可以滿足基本的需求哦!還可以隨意定製!喜歡記得點贊,收藏,轉發哈!
膜拜的大神:
實在抱歉,記得有看到過大神有類似思路的,只是想不起了,如果大神看到,請通知我,我會在這裡寫上大神的部落格