使用View來搭建UI
本文主要是想表達一下在Android UI開發中我對View
的看法,個人經驗有限,有什麼問題歡迎一塊討論。
如果說Activity
是Android提供的頁面容器的話,那View
就是最基礎的UI元件(有點是廢話)。什麼意思呢?我認為絕大部分UI開發工作都可以使用View來完成。下文就結合我工作中的一些實際case來談一下View
的使用。(當然不是講怎麼自定義View
)
Fragment與View
Google
推薦使用Fragment
來在Activity
中搭建碎片化UI,但我感覺完全可以使用View
來代替Fragment
完成這個功能,並且程式碼簡單易懂可維護、bug也少。
為什麼不推薦使用Fragment
呢?可以看一下這篇文章:
Fragment
都有哪些坑呢?下面這兩篇文章瞭解一下:
當然我也是踩過Fragment
很多坑的,比如在使用ViewPager + Fragment + LifeCycle
這種架構時,ViewPager
切換Fragment
時,LifeCycle
根本沒做通知。
View相較於Fragment的優勢
當然都是一些個人觀點
-
View
複用性更強,不像Fragment
那樣需要依賴於FrameLayout
。其實Fragment
的UI顯示邏輯也是交給View的呀(有點是廢話)。 -
View
使用起來更靈活,你可以對他進行各種操作,比如remove/add、巢狀在任何地方等等,而且回撥寫起來更扁平。 -
使用
View
不需要理會複雜的生命週期,其實你大部分情況下View的生命週期
已經足夠你使用了,大不了寫個方法讓Activity
來回調就可以了。 -
都是用來顯示UI,
View
相較於Fragment
更直接更純粹,更輕量級,當然bug更少。
我們只需要使用View
建立響應式UI,實現回退棧以及螢幕事件的處理,不用Fragment
也能滿足實際開發的需求。《出自Square:從今天開始拋棄Fragment吧!
》
View使用實戰
下面從幾個不同的case來講一下在實際場景中View
的使用。
使用View來代替Fragment
很簡單,只需要自定義一個ViewGroup
就Ok了。不過對於一些邏輯複雜的頁面我們會引入MVP
,那麼如何讓Presenter
來感知生命週期事件呢?在使用Fragment
時,我們可以直接感受生命週期,對於View
的話我們可以引入LifeCycle
,即View
感知Activity
的生命週期,其實Fragment
的生命週期也是跟著Activity
走的呀。
View的Presenter對生命週期的感知
假設專案引入了LifeCycle
, 那麼可以這樣設計:
Presenter實現LifecycleObserver
class DemoPresenter(demoPage:DemoPageProtocol) : LifecycleObserver { private val model... @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun destroy() { model.clearDisposable() //釋放model中的網路資源 } }
View把Presenter註冊到LifeCycle中
class DemoPage(context: Context) : LinearLayout(context), DemoPageProtocol{ private val presenter: DemoPresenterby lazy { DemoPresenter(this) } init { LayoutInflater.from(context).inflate(R.layout.demo_page, this)//這裡可以使用merge來消除冗餘的父節點 (context as AppCompatActivity).lifecycle.addObserver(presenter) } }
這裡的強轉其實是沒有問題的,我們使用的Activity
基本都繼承自AppCompatActivity
。(當然你要知道你在寫什麼)
RecyclerView中的View
RecyclerView
是使用頻率非常高的一個控制元件,我個人比較推薦的一種寫法是:
直接寫View
,View
到ViewHolder
的對映交給Adapter
來完成。
具體封裝方式可以參考下面這篇文章:
View中可以做一些簡單的網路請求
RecyclerView
中的View
有時是會含有一些簡單的網路事件的比如點贊、關注等等。我一般是直接寫在View
中,因為我感覺這樣寫起來更直觀。但是網路請求在什麼時候釋放呢?我感覺可以在View onDetachedFromWindow
時把這些網路事件釋放掉:
override fun onDetachedFromWindow() { super.onDetachedFromWindow() disposableList.forEach {//釋放 disposable, 防止記憶體洩漏問題 it.dispose() } }
PopupWindow與View
為什麼說這個呢? 其實是對應到DialogFragment
。對於這個我想說還是別用。它的內部實現是: Fragment->(Dialog ->(PhoneWindow))。這3個東西加在一起就夠頭疼的了。
所以對於一些側滑彈窗、上下操作彈窗可以使用PopupWindow+View
來實現,不過PopupWindow
在這種場景下也有一些問題,但相較於DialogFragment
少一些:
PopupWindow不顯示的問題
:其實這篇文章也沒有完全解決,在某些手機上你一定要定死寬高,PopupWindow
才可以顯示出來。
PopupWindow的彈出位置
:PopupWindow彈出位置的計算。其實我目前使用的都是基於參照物Anchor(一般我都是取Activity.window.decorView
)的相對位置來展示的。
在具體使用時最好採用組合的方式,比如:
class SimplePopupWindow(val context: Context, val mContentView: View) { private var mWindow = PopupWindow() init { mWindow.apply { contentView = mContentView height = UIUtil.getScreenHeight() width = UIUtil.getScreenWidth() ... } } fun show(anchor: View) { mWindow.update() mWindow.showAsDropDown(anchor, 0, 0, Gravity.TOP) } ..... }
View的生命週期
想較於Fragment
的生命週期來說,View
的生命週期就很弱了,View
的生命週期相關方法可以參考下面這篇文章:
這裡重點提一下:
對於View.onAttatchToWindow
方法你應該知道它是在ViewRootImpl.performTraversal()
中開始回撥的,具體回撥時機是measure
前。
但是當使用View
來搭建頁面級UI時,像onAttatchToWindow
、onDetachedFromWindow
這種方法可能就不是很適用了。我的一般操作是寫個方法直接讓上層(Activity
)來呼叫:
DemoPage
class DemoPage(context: Context) : LinearLayout(context),DemoPageProtocol{ //View被展示時,Activtiy回撥這個方法 fun show() { //load data } //對應Activtiy的onResume fun onResume(){ } }
對於ViewPager+View
的架構來說,完成View
的懶載入並不是什麼難事。
Dialog與View
我曾經遇到過這樣一個需求:
專案中的一個全域性loading
是使用Dialog
來實現的,這就造成在loading出現時介面是鎖死的,在這種情況下如果網路比較慢的話,很容易就讓使用者以為我們的app死掉了。所以需要實現這樣一個全域性loading:在它出現的時候不能鎖死介面,並且使用者點選返回鍵可以關掉它。
我是怎麼實現的呢?其實我的實現方法比較取巧,好不好先不說,說一下思路吧:
-
自定義一個
ViewGroup
, 它出現時會展示loading動畫。 -
其內部含有一個看不見的
EditText
,用於監聽使用者是否點選了返回鍵,點選了返回鍵的話就關掉loading。 -
可以自動attach到
Activity
的DecorView
上,當然也可以當做一個普通的View
由其他元件來使用。
我個人感覺使用View
來實現這個loading,相較於Dialog
來說,還是很靈活的,有興趣的同學可以看一下程式碼: