項(xiàng)目簡(jiǎn)介
玩Android demo。用Jetpack MVVM開發(fā)架構(gòu)、單Activity多Fragment項(xiàng)目設(shè)計(jì),項(xiàng)目結(jié)構(gòu)清晰,代碼簡(jiǎn)潔優(yōu)雅,追求最官方的實(shí)現(xiàn)方式。用到以下知識(shí)點(diǎn):
LiveData、ViewModel、DataBinding(包括雙向綁定、BindingAdapter的使用)、ViewBinding、coroutines(包含flow、suspend、livedata協(xié)程構(gòu)造器、flow協(xié)程構(gòu)造器的使用)、Hilt、Paging3(包含RemoteMediator、加載狀態(tài))、Room、Navigation(通過ViewModel共享數(shù)據(jù))、Banner(kotlin簡(jiǎn)單實(shí)現(xiàn))、TabLayout、BottomNavigationView、RecycleView(包含ListAdapter、ConcatAdapter、PagingDataAdapter的使用)、ViewPager2、Glide、Cookie、Retrofit2、啟動(dòng)頁面、深色主題、沉浸式模式、Kotlin高階函數(shù)。
項(xiàng)目截圖(默認(rèn)主題、深色主題)
項(xiàng)目參考demo
- Sunflower
- architecture-components-samples
- views-widgets-samples
- user-interface-samples
- architecture-samples
- compose-samples: 本demo中暫時(shí)不涉及Compose功能
項(xiàng)目知識(shí)點(diǎn)
LiveData
- LiveData文檔
- LiveDataSample:
- 持有可被觀察的類類似于EventBus或者RxJava。LiveData是一種可感知生命周期的組件
- LiveData與MutableLiveData區(qū)別
- LiveData使用
- 理解協(xié)程、LiveData 和 Flow
- Google 推薦在 MVVM 架構(gòu)中使用 Kotlin Flow
- 關(guān)于Retrofit和LiveData相關(guān)參考demo:GithubBrowserSample[]
ViewModel
- ViewModel文檔
-
ViewModel 四種集成方式,即:
- ViewModel 中的 Saved State —— 后臺(tái)進(jìn)程重啟時(shí),ViewModel 的數(shù)據(jù)恢復(fù);
- 在 NavGraph 中使用 ViewModel —— ViewModel 與導(dǎo)航 (Navigation) 組件庫(kù)的集成;
- ViewModel 配合數(shù)據(jù)綁定 (data-binding) —— 通過使用 ViewModel 和 LiveData 簡(jiǎn)化數(shù)據(jù)綁定;
- viewModelScope —— Kotlin 協(xié)程與 ViewModel 的集成。
- 在Activity或者Fragment中如何處理ViewModel的三種方式(沒太懂)
ViewBinding
DataBinding
-
DataBinding文檔
- 取代findviewbyId,類似于Butterknife。
coroutines
-
理解協(xié)程、LiveData 和 Flow
- liveData 協(xié)程構(gòu)造方法提供了一個(gè)協(xié)程代碼塊,這個(gè)塊就是 LiveData 的作用域,當(dāng) LiveData 被觀察的時(shí)候,里面的操作就會(huì)被執(zhí)行,當(dāng) LiveData 不再被使用時(shí),里面的操作就會(huì)取消。 而且該協(xié)程構(gòu)造方法產(chǎn)生的是一個(gè)不可變的LiveData,可以直接暴露給對(duì)應(yīng)的視圖使用。而 emit() 方法則用來更新 LiveData 的數(shù)據(jù)。
- 一個(gè)常見用例,比如當(dāng)用戶在 UI 中選中一些元素,然后將這些選中的內(nèi)容顯示出來。一個(gè)常見的做法是,把被選中的項(xiàng)目的 ID 保存在一個(gè) MutableLiveData 里,然后運(yùn)行 switchMap?,F(xiàn)在在 switchMap 里,您也可以使用協(xié)程構(gòu)造方法:
private val itemId = MutableLiveData<String>() val result = itemId.switchMap { liveData { emit(fetchItem(it)) } } - Google 推薦在 MVVM 架構(gòu)中使用 Kotlin Flow
- 圖解協(xié)程原理
Hilt
- hilt 和 Koin
Paging
Paging 庫(kù) 3.0.0正式版已發(fā)布,普天同慶!Paging 庫(kù)可幫助您加載和顯示來自本地存儲(chǔ)或網(wǎng)絡(luò)中更大的數(shù)據(jù)集中的數(shù)據(jù)頁面。此方法可讓您的應(yīng)用更高效地利用網(wǎng)絡(luò)帶寬和系統(tǒng)資源。Paging 庫(kù)的組件旨在契合推薦的 Android 應(yīng)用架構(gòu),流暢集成其他 Jetpack 組件,并提供一流的 Kotlin 支持。
-
官方demo:
- PagingSample : 本地?cái)?shù)據(jù)庫(kù)的demo
- PagingWithNetworkSample : 網(wǎng)絡(luò)數(shù)據(jù)的demo
-
Paging 庫(kù)包含以下功能:
- 分頁數(shù)據(jù)的內(nèi)存中緩存。該功能可確保您的應(yīng)用在處理分頁數(shù)據(jù)時(shí)高效利用系統(tǒng)資源。
- 內(nèi)置的請(qǐng)求重復(fù)信息刪除功能,可確保您的應(yīng)用高效利用網(wǎng)絡(luò)帶寬和系統(tǒng)資源。
- 可配置的 RecyclerView 適配器,會(huì)在用戶滾動(dòng)到已加載數(shù)據(jù)的末尾時(shí)自動(dòng)請(qǐng)求數(shù)據(jù)。
- 對(duì) Kotlin 協(xié)程和 Flow 以及 LiveData 和 RxJava 的一流支持。
- 內(nèi)置對(duì)錯(cuò)誤處理功能的支持,包括刷新和重試功能。
-
Paging 組件及其在應(yīng)用架構(gòu)的集成:
paging3-library-architecture.svg -
定義數(shù)據(jù)源 : 數(shù)據(jù)源的定義取決于您從哪里加載數(shù)據(jù)。您僅需實(shí)現(xiàn) PagingSource 或者 PagingSource 與 RemoteMediator 的組合:
- 如果您從單個(gè)源加載數(shù)據(jù),例如網(wǎng)絡(luò)、本地?cái)?shù)據(jù)、文件、內(nèi)存緩存等(不只是網(wǎng)絡(luò)和數(shù)據(jù)庫(kù),其他如文件也可以使用Paging),實(shí)現(xiàn) PagingSource 即可,如果您使用了 Room,從 2.3.0-alpha 開始,它將默認(rèn)為您實(shí)現(xiàn) PagingSource。
- 如果您從一個(gè)多層級(jí)數(shù)據(jù)源加載數(shù)據(jù),就像帶有本地?cái)?shù)據(jù)庫(kù)緩存的網(wǎng)絡(luò)數(shù)據(jù)源那樣。那么您需要實(shí)現(xiàn) RemoteMediator 來合并兩個(gè)數(shù)據(jù)源到一個(gè)本地?cái)?shù)據(jù)庫(kù)緩存的 PagingSource 中。
-
PagingSource :
- PagingSource 可以定義一個(gè)分頁數(shù)據(jù)的數(shù)據(jù)源,以及從該數(shù)據(jù)源獲取數(shù)據(jù)的方式。
- LoadParams:PagingSource 的 密封類(sealed),包含有關(guān)要執(zhí)行的加載操作的信息,其中包括要加載的鍵和要加載的項(xiàng)數(shù)。作為load()函數(shù)的參數(shù)使用
- LoadResult:PagingSource 的 密封類(sealed),包含加載操作的結(jié)果。LoadResult 是一個(gè)密封的類,根據(jù) load() 調(diào)用是否成功。作為load()函數(shù)的返回值
- getRefreshKey(): 該方法接受 PagingState 對(duì)象作為參數(shù),并且當(dāng)數(shù)據(jù)在初始加載后刷新或失效時(shí),該方法會(huì)返回要傳遞給 load() 方法的鍵。在后續(xù)刷新數(shù)據(jù)時(shí),Paging 庫(kù)會(huì)自動(dòng)調(diào)用此方法。
-
load(): 下圖說明了load() 函數(shù)如何接收每次加載的鍵并為后續(xù)加載提供鍵:
paging3-source-load.svg - 代碼示例:
// 自定義PagingSource類 private const val ARTICLE_STARTING_PAGE_INDEX = 0 class HomeArticlePagingSource( private val api: WanJetpackApi ) : PagingSource<Int, ApiArticle>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ApiArticle> { val page = params.key ?: ARTICLE_STARTING_PAGE_INDEX return try { val response = api.getHomeArticle(page) val datas = response.data.datas LoadResult.Page( data = datas, prevKey = if (page == ARTICLE_STARTING_PAGE_INDEX) null else page - 1, nextKey = if (page == response.data.pageCount) null else page + 1, ) } catch (exception: Exception) { LoadResult.Error(exception) } } override fun getRefreshKey(state: PagingState<Int, ApiArticle>): Int? { return null } }
-
PagingData :
- 分頁數(shù)據(jù)的容器被稱為 PagingData,每次刷新數(shù)據(jù)時(shí),都會(huì)創(chuàng)建一個(gè) PagingData 的實(shí)例。如果要?jiǎng)?chuàng)建 PagingData 數(shù)據(jù)流,您需要?jiǎng)?chuàng)建一個(gè) Pager 實(shí)例,并提供一個(gè) PagingConfig 配置對(duì)象和一個(gè)可以告訴 Pager 如何獲取您實(shí)現(xiàn)的 PagerSource 的實(shí)例的函數(shù),以供 Pager 使用。
- Pager 類提供的方法可顯示來自 PagingSource 的 PagingData 對(duì)象的響應(yīng)式流。Paging 庫(kù)支持使用多種流類型,包括 Flow、LiveData 以及 RxJava 中的 Flowable 和 Observable 類型。
- 通過 Pager().flow可以返回Flow<PagingData<ApiArticle>>。然后在ViewModel中.cachedIn(viewModelScope), cachedIn()運(yùn)算符使數(shù)據(jù)流可共享,并使用提供的 CoroutineScope 緩存加載的數(shù)據(jù)
- 代碼示例: (注:Pager 的 remoteMediator 參數(shù)可選項(xiàng), RemoteMediator 是重點(diǎn))
//Repository: fun getHomeArticle(): Flow<PagingData<ApiArticle>> { return Pager( config = PagingConfig(enablePlaceholders = false, pageSize = HOME_ARTICLE_PAGE_SIZE), pagingSourceFactory = { HomeArticlePagingSource(api) } ).flow }//ViewModel: fun getHomeArticle(): Flow<PagingData<ApiArticle>> { val newResult: Flow<PagingData<ApiArticle>> = repository.getHomeArticle().cachedIn(viewModelScope) currentArticleResult = newResult return newResult }
-
PagingDataAdapter :
- 與定義 RecyclerView 列表 Adapter 時(shí)的通常做法相同:必須定義 onCreateViewHolder() 和 onBindViewHolder() 方法;指定 ViewHoler 和 DiffUtil.ItemCallback
- Adapter 及 UI ( Activity、Fragment )中的相關(guān)代碼略。
-
LoadType : 是個(gè) enum 類,包含三種狀態(tài):REFRESH、PREPEND、APPEND。在 PagingSource 的 LoadParams 類中用到。
- 官方介紹:Type of load a [PagingData] can trigger a [PagingSource] to perform.
- REFRESH:[PagingData] content being refreshed, which can be a result of [PagingSource] invalidation, refresh that may contain content updates, or the initial load.
- PREPEND:Load at the start of a [PagingData].
- APPEND:Load at the end of a [PagingData].
-
LoadState : 是個(gè) sealed(密封) 類。
- 官方介紹:LoadState of a PagedList load - associated with a [LoadType].
- [LoadState] of any [LoadType] may be observed for UI purposes by registering a listener via [androidx.paging.PagingDataAdapter.addLoadStateListener] or [androidx.paging.AsyncPagingDataDiffer.addLoadStateListener]
- Paging 庫(kù)通過 LoadState 對(duì)象公開可在界面中使用的加載狀態(tài)。LoadState 根據(jù)當(dāng)前的加載狀態(tài)采用以下三種形式之一:
- 如果沒有正在執(zhí)行的加載操作且沒有錯(cuò)誤,則 LoadState 為 LoadState.NotLoading 對(duì)象。
- 如果有正在執(zhí)行的加載操作,則 LoadState 為 LoadState.Loading 對(duì)象。
- 如果出現(xiàn)錯(cuò)誤,則 LoadState 為 LoadState.Error 對(duì)象。
加載狀態(tài)的三個(gè)場(chǎng)景:下拉刷新、上拉加載更多、首次進(jìn)入頁面中間的滾動(dòng)條(及加載失敗提醒)
-
顯示加載狀態(tài) : 可通過兩種方法在界面中使用 LoadState:使用監(jiān)聽器,以及使用特殊的列表適配器在 RecyclerView 列表中直接顯示加載狀態(tài)。
- 方法一、 使用監(jiān)聽器獲取加載狀態(tài): 為了獲取加載狀態(tài)以用于界面中的一般用途,PagingDataAdapter 中提供了 addLoadStateListener()、loadStateFlow 兩種方式。來自 loadStateFlow 或 addLoadStateListener() 的更新可確保與界面的更新保持同步。這意味著,如果您收到 NotLoading.Incomplete 的 LoadState,則可以確定加載已完成,并且界面也已相應(yīng)更新。
// addLoadStateListener 方式。 articleAdapter.addLoadStateListener { when (it.refresh) { is LoadState.NotLoading -> { progressBar.visibility = View.INVISIBLE recyclerView.visibility = View.VISIBLE } is LoadState.Loading -> { progressBar.visibility = View.VISIBLE recyclerView.visibility = View.INVISIBLE } is LoadState.Error -> { val state = it.refresh as LoadState.Error progressBar.visibility = View.INVISIBLE Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show() } } }// loadStateFlow 方式 // collectLatest 是個(gè) suspend 函數(shù),所以要在協(xié)程或者另一個(gè) suspend 中調(diào)用 lifecycleScope.launch { pagingAdapter.loadStateFlow.collectLatest { progressBar.isVisible = it.refresh is LoadState.Loading retry.isVisible = it.refresh !is LoadState.Loading errorMsg.isVisible = it.refresh is LoadState.Error } } - 方法二、 使用適配器呈現(xiàn)加載狀態(tài): Paging 庫(kù)提供了另一個(gè)名為 LoadStateAdapter 的列表適配器,用于直接在顯示的分頁數(shù)據(jù)列表中呈現(xiàn)加載狀態(tài)。其實(shí)該方法就是在PagingDataAdapter中把a(bǔ)ddLoadStateListener()和ConcatAdapter封裝了一下
- 首先,創(chuàng)建一個(gè)實(shí)現(xiàn) LoadStateAdapter 的類,并定義 onCreateViewHolder() 和 onBindViewHolder() 方法:
class LoadStateViewHolder( parent: ViewGroup, retry: () -> Unit ) : RecyclerView.ViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.load_state_item, parent, false) ) { private val binding = LoadStateItemBinding.bind(itemView) private val progressBar: ProgressBar = binding.progressBar private val errorMsg: TextView = binding.errorMsg private val retry: Button = binding.retryButton .also { it.setOnClickListener { retry() } } fun bind(loadState: LoadState) { if (loadState is LoadState.Error) { errorMsg.text = loadState.error.localizedMessage } progressBar.isVisible = loadState is LoadState.Loading retry.isVisible = loadState is LoadState.Error errorMsg.isVisible = loadState is LoadState.Error } } // Adapter that displays a loading spinner when // state = LoadState.Loading, and an error message and retry // button when state is LoadState.Error. class ExampleLoadStateAdapter( private val retry: () -> Unit ) : LoadStateAdapter<LoadStateViewHolder>() { override fun onCreateViewHolder( parent: ViewGroup, loadState: LoadState ) = LoadStateViewHolder(parent, retry) override fun onBindViewHolder( holder: LoadStateViewHolder, loadState: LoadState ) = holder.bind(loadState) } - 然后,從 PagingDataAdapter 對(duì)象調(diào)用 withLoadStateHeaderAndFooter() 方法:
pagingAdapter .withLoadStateHeaderAndFooter( header = ExampleLoadStateAdapter(adapter::retry), footer = ExampleLoadStateAdapter(adapter::retry) ) - 如果您只想讓 RecyclerView 在頁眉或頁腳中顯示加載狀態(tài),則可以調(diào)用 withLoadStateHeader() 或 withLoadStateFooter()。 關(guān)于withLoadStateHeaderAndFooter()、withLoadStateHeader() 和 withLoadStateFooter()的實(shí)現(xiàn),通過源碼發(fā)現(xiàn),其實(shí)就是用的PagingDataAdapter.addLoadStateListener()方案,只不過是通過ConcatAdapter封裝下。即:在PagingDataAdapter中把a(bǔ)ddLoadStateListener()和ConcatAdapter封裝了一下,且返回值是ConcatAdapter
- 注意:由于withLoadStateHeaderAndFooter()、withLoadStateHeader() 和 withLoadStateFooter()返回的是ConcatAdapter,所以如果已經(jīng)用構(gòu)造函數(shù)ConcatAdapter(firstAdapter, articleAdapter)的話,再用withLoadState···添加頁眉頁腳會(huì)失敗,因?yàn)橛脀ithLoadState···返回的也是ConcatAdapter就有兩個(gè)ConcatAdapter了。這個(gè)時(shí)候正確的做法是用withLoadState···創(chuàng)建ConcatAdapter,然后再用concatAdapter.addAdapter(0,firstAdapter)添加其它的adapter,且調(diào)用concatAdapter.addAdapter的位置在binding.articleList.adapter = concatAdapter前后都可以。
- 首先,創(chuàng)建一個(gè)實(shí)現(xiàn) LoadStateAdapter 的類,并定義 onCreateViewHolder() 和 onBindViewHolder() 方法:
- 方法一、 使用監(jiān)聽器獲取加載狀態(tài): 為了獲取加載狀態(tài)以用于界面中的一般用途,PagingDataAdapter 中提供了 addLoadStateListener()、loadStateFlow 兩種方式。來自 loadStateFlow 或 addLoadStateListener() 的更新可確保與界面的更新保持同步。這意味著,如果您收到 NotLoading.Incomplete 的 LoadState,則可以確定加載已完成,并且界面也已相應(yīng)更新。
Pager : Pager().flow 把 PagingSource 轉(zhuǎn)換為 PagingData。在Repository中用到
-
RemoteMediator : 在Pager()中用到。
- 當(dāng)您從一個(gè)多層級(jí)數(shù)據(jù)源加載數(shù)據(jù)時(shí),應(yīng)當(dāng)實(shí)現(xiàn)一個(gè) RemoteMediator。
- 一般用法為從網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)并存入數(shù)據(jù)庫(kù)。每當(dāng)數(shù)據(jù)庫(kù)中沒有數(shù)據(jù)可以被展示時(shí),就會(huì)觸發(fā) load() 方法。基于 PagingState 和 LoadType,我們可以構(gòu)造下一頁的數(shù)據(jù)請(qǐng)求。
PagingConfig : 在Pager()中用到
-
PagingState : 在自定義 PagingSource 的 getRefreshKey()方法中用到,在自定義RemoteMediator的load()方法中也用到了。
- 官方介紹:Snapshot state of Paging system including the loaded [pages], the last accessed [anchorPosition], and the [config] used.
-
參考博客:目前Paging已經(jīng)發(fā)布3.0正式版,下面這個(gè)博客是alpha版本的,但可以參考:
Room
DataStore
App Startup
WorkManager
compose
Navigation
- Navigation文檔
- NavigationAdvancedSample
- 和路由框架ARouter的區(qū)別:ARouter主要是用于Activity路由的框架,采用的是APT技術(shù),可用于組件化改造。而Navigation主要是用于Fragment路由導(dǎo)航的框架。
- Jetpack 之 Navigation 全面剖析
- 底部導(dǎo)航欄1
- 底部導(dǎo)航欄2
- 用戶登錄場(chǎng)景
- Safe Args 導(dǎo)航
- Fragment間轉(zhuǎn)場(chǎng)動(dòng)畫:Android Material 組件 1.2.0 現(xiàn)已發(fā)布
Preference
RecyclerView
- RecyclerView 庫(kù)
- 官方文檔
-
官方demo
- RecyclerView: java版本普通demo
- RecyclerViewAnimations: 新增、刪除、更新 item的demo
- RecyclerViewKotlin: ConcatAdapter的demo
- 實(shí)現(xiàn)了ConcatAdapter,能規(guī)避嵌套滑動(dòng)
- 用Kotlin高階函數(shù)處理 RecyclerView 中的點(diǎn)擊事件
- 實(shí)現(xiàn)了新增、刪除item的方案及更新item動(dòng)畫
- RecyclerViewSimple: kotlin版本普通demo
- 默認(rèn)的adapter:RecyclerView.Adapter:認(rèn)識(shí) RecyclerView
- ListAdapter:繼承RecyclerView.Adapter:在 RecyclerView 中使用 ListAdapter
- ConcatAdapter:
- PagingDataAdapter:繼承RecyclerView.Adapter
滑動(dòng)刷新
- 滑動(dòng)刷新: 一般滑動(dòng)刷新用于RecyclerView中的下拉刷新和上拉加載更多。
- 官方文檔
- 官方demo:
- 滑動(dòng)刷新界面實(shí)現(xiàn)方案:
- 三方框架:SmartRefreshLayout
- 自己實(shí)現(xiàn)有三種方案:
- 方案一: 可以在RecyclerView外層自定義一個(gè)布局,里面放三個(gè)控件:HeaderView、RecyclerView、FooterView。 結(jié)合SwipeRefreshLayout的話,只需要寫個(gè)FooterView就行了。Android 簡(jiǎn)單易上手的下拉刷新控件、Android RecyclerView下拉刷新 & 上拉加載更多
- 方案二: 可以作為RecyclerView的兩個(gè)item處理,通過不同的Type類型區(qū)分
- 方案三: 可以通過ConcatAdapter配置:使用 ConcatAdapter 順序連接其他 Adapter
- 方案四: 可以直接用 PagingDataAdapter.withLoadStateFooter()加載頁腳,但是下拉刷新還要自己實(shí)現(xiàn)。查詢PagingDataAdapter中的實(shí)現(xiàn)方式發(fā)現(xiàn),其實(shí)該方法也就是方案三,只是在PagingDataAdapter中已經(jīng)封裝好了。
- 關(guān)于下拉刷新,還可以利用左滑刪除的思想實(shí)現(xiàn),但是體驗(yàn)不是特別理想,暫時(shí)pass該方案
- 滑動(dòng)刷新功能實(shí)現(xiàn)方案:
- 如果是 ConcatAdapter、PagingDataAdapter : 即用了 Paging3 ,相關(guān)說明參考上面的Paging 庫(kù)說明。
- 如果是 ListAdapter 、 RecyclerView.Adapter:
- 下拉刷新、左滑刪除參考demo:
加載狀態(tài)
- 加載狀態(tài)的幾個(gè)場(chǎng)景:下拉刷新、上拉加載更多、底部的已加載全部?jī)?nèi)容、首次進(jìn)入頁面的加載狀態(tài)(及加載失敗提醒)
- 下拉刷新、上拉加載更多:略
- 首次進(jìn)入頁面的加載狀態(tài):
- 底部的已加載全部?jī)?nèi)容:方案比較多,個(gè)人比較傾向下面兩種方案
- 方案一:通過withLoadStateFooter實(shí)現(xiàn),和上拉加載更多用同一套布局,同一個(gè)adapter?!緟⒖急綿emo】
- 方案二:通過ConcatAdapter.addAdapter實(shí)現(xiàn),專門顯示加載更多
動(dòng)畫
- Animation 動(dòng)畫: 下拉刷新場(chǎng)景通過屬性動(dòng)畫實(shí)現(xiàn)
- 官方文檔
-
屬性動(dòng)畫:
- ValueAnimator: 屬性動(dòng)畫的主計(jì)時(shí)引擎,它也可計(jì)算要添加動(dòng)畫效果的屬性的值。它具有計(jì)算動(dòng)畫值所需的所有核心功能,同時(shí)包含每個(gè)動(dòng)畫的計(jì)時(shí)詳情、有關(guān)動(dòng)畫是否重復(fù)播放的信息、用于接收更新事件的監(jiān)聽器以及設(shè)置待評(píng)估自定義類型的功能。為屬性添加動(dòng)畫效果分為兩個(gè)步驟:計(jì)算添加動(dòng)畫效果之后的值,以及對(duì)要添加動(dòng)畫效果的對(duì)象和屬性設(shè)置這些值。ValueAnimator 不會(huì)執(zhí)行第二個(gè)步驟,因此,您必須監(jiān)聽由 ValueAnimator 計(jì)算的值的更新情況,并使用您自己的邏輯修改要添加動(dòng)畫效果的對(duì)象。如需了解詳情,請(qǐng)參閱使用 ValueAnimator 添加動(dòng)畫效果部分。
- ObjectAnimator: ValueAnimator 的子類,用于設(shè)置目標(biāo)對(duì)象和對(duì)象屬性以添加動(dòng)畫效果。此類會(huì)在計(jì)算出動(dòng)畫的新值后相應(yīng)地更新屬性。在大多數(shù)情況下,您不妨使用 ObjectAnimator,因?yàn)樗梢詷O大地簡(jiǎn)化對(duì)目標(biāo)對(duì)象的值添加動(dòng)畫效果這一過程。不過,有時(shí)您需要直接使用 ValueAnimator,因?yàn)?ObjectAnimator 存在其他一些限制,例如要求目標(biāo)對(duì)象具有特定的訪問器方法。
- AnimationSet: 此類提供一種將動(dòng)畫分組在一起的機(jī)制,以使它們彼此相對(duì)運(yùn)行。您可以將動(dòng)畫設(shè)置為一起播放、按順序播放或者在指定的延遲時(shí)間后播放。如需了解詳情,請(qǐng)參閱使用 AnimatorSet 編排多個(gè)動(dòng)畫部分。
- LayoutTransition:
- LayoutAnimations:
ViewPager2
- ViewPager2 庫(kù)
- 官方文檔
-
官方demo
- 官方demo中的ViewPager2 with a Preview of Next/Prev Page 相當(dāng)于Banner中類似的場(chǎng)景
- 官方demo中的ViewPager2 with a Nested RecyclerViews 場(chǎng)景很好,提供了解決嵌套滑動(dòng)的方案
- ViewPager2 底層使用 RecycleView 實(shí)現(xiàn)的,所以這里不再使用 PagerAdapter 而是使用了 RecyclerView.Adapter
- 對(duì)應(yīng)的fragment用的是 FragmentStateAdapter,而不是 FragmentStatePagerAdapter、FragmentPagerAdapter之類的
Banner
- Banner:其實(shí)就是 ViewPager 的應(yīng)用
- 三方庫(kù):
- 自己實(shí)現(xiàn)方案:
- 讓Banner和RecyclerView分開: 通過NestedScrollView里包裹ViewPager2和RecyclerView的話,會(huì)有滑動(dòng)卡頓的問題,即使加上android:nestedScrollingEnabled="false"屬性,除非再加上setHasFixedSize(true),但是還會(huì)有其他的問題:加上setHasFixedSize(true)后,界面的數(shù)據(jù)只顯示一頁了。故此方案暫時(shí)行不通了。本方案相關(guān)代碼
binding.articleList.setHasFixedSize(true) binding.articleList.isNestedScrollingEnabled = false - 讓Banner成為RecyclerView的一部分:
- 如果Banner在頂部:banner在頂部的話,就做header
- 如果Banner在中間:在中間的話,就type,或者對(duì)adapter做一個(gè)擴(kuò)展,做一個(gè)可以在中間插入的類似header。畢竟type的話,寫起來也蠻麻煩的
- 通過 ConcatAdapter 實(shí)現(xiàn):
- 本demo就是用的該方案,demo中通過HomeFirstAdapter添加RecyclerView的ConcatAdapter中,通過HomeBannerAdapter實(shí)現(xiàn)ViewPager2的adapter。
- 通過上述的方式加上ViewPager2之后,ViewPager2沒有影響RecyclerView的功能,RecyclerView上下滑動(dòng)流暢;但是ViewPager2不能滑動(dòng),因?yàn)槭录籖ecyclerView攔截了。故需新增自定義布局 NestedFrameLayout 嵌套在ViewPager2之上,在 NestedFrameLayout 去處理父類的事件分發(fā),即當(dāng)左右滑動(dòng) NestedFrameLayout 時(shí),執(zhí)行 NestedFrameLayout 的parent.requestDisallowInterceptTouchEvent(true)方法,讓ViewPager2消費(fèi)事件。
- 通過 MultiTypeAdapter 實(shí)現(xiàn):暫時(shí)沒有驗(yàn)證
- 工行融e購(gòu)實(shí)現(xiàn)方案:首頁除了viewpager功能都放在AppBarLayout里面,但是這樣TabLayout可能就要和融e購(gòu)一樣放在下面了,不是想要的。用ConcatAdapter也可以實(shí)現(xiàn)工行融e購(gòu)的首頁效果。
- 京東首頁實(shí)現(xiàn)方案:自定義控件實(shí)現(xiàn)。用ConcatAdapter也能實(shí)現(xiàn)京東首頁效果
- 讓Banner和RecyclerView分開: 通過NestedScrollView里包裹ViewPager2和RecyclerView的話,會(huì)有滑動(dòng)卡頓的問題,即使加上android:nestedScrollingEnabled="false"屬性,除非再加上setHasFixedSize(true),但是還會(huì)有其他的問題:加上setHasFixedSize(true)后,界面的數(shù)據(jù)只顯示一頁了。故此方案暫時(shí)行不通了。本方案相關(guān)代碼
NestedScrollView
- 直接在 NestedScrollView 中放入 ViewPager2 和 RecyclerView 時(shí),會(huì)出現(xiàn)滑動(dòng)卡頓。解決方案參考
- NestedScrollView
- 事件沖突的原因:Android 的事件分發(fā)機(jī)制中,只要有一個(gè)控件消費(fèi)了事件,其他控件就沒辦法再接收到這個(gè)事件了。因此,當(dāng)有嵌套滑動(dòng)場(chǎng)景時(shí),我們都需要自己手動(dòng)解決事件沖突。而在 Android 5.0 Lollipop 之后,Google 官方通過 嵌套滑動(dòng)機(jī)制 解決了傳統(tǒng) Android 事件分發(fā)無法共享事件這個(gè)問題。
- 嵌套滑動(dòng)機(jī)制:嵌套滑動(dòng)機(jī)制 的基本原理可以認(rèn)為是事件共享,即當(dāng)子控件接收到滑動(dòng)事件,準(zhǔn)備要滑動(dòng)時(shí),會(huì)先通知父控件(startNestedScroll);然后在滑動(dòng)之前,會(huì)先詢問父控件是否要滑動(dòng)(dispatchNestedPreScroll);如果父控件響應(yīng)該事件進(jìn)行了滑動(dòng),那么就會(huì)通知子控件它具體消耗了多少滑動(dòng)距離;然后交由子控件處理剩余的滑動(dòng)距離;最后子控件滑動(dòng)結(jié)束后,如果滑動(dòng)距離還有剩余,就會(huì)再問一下父控件是否需要在繼續(xù)滑動(dòng)剩下的距離(dispatchNestedScroll)...
TabLayout
- TabLayout
- 和ViewPager2、Fragment應(yīng)用
BottomNavigationView
- BottomNavigationView
- 和Navigation、Fragment、ViewPager2應(yīng)用
Constraint Layout
CoordinatorLayout、NestedScrollView、CollapsingToolbarLayout、AppBarLayout、MaterialToolbart
-
待優(yōu)化場(chǎng)景,搜索場(chǎng)景:search在 Android 系統(tǒng)的協(xié)助下使用搜索對(duì)話框或搜索微件傳遞搜索查詢
- 搜索對(duì)話框
- 搜索微件
Glide
- 和Coil對(duì)比,建議換成Coil加載圖片
Cookie
- CookieManager
- 本demo中,和收藏相關(guān)都需要登錄操作,建議登錄將返回的cookie(其中包含賬號(hào)、密碼)持久化到本地即可。
WebView
- WebView 庫(kù)
- 官方文檔
- 官方demo
- 本demo中跳轉(zhuǎn)到WebFragment是通過 Bundle 傳遞參數(shù),沒有用通過 Navigation 的 Safe Args 導(dǎo)航實(shí)現(xiàn)
- 本demo中的WebView適配了深色主題。
啟動(dòng)界面
- 方案:通過windowSplashscreenContent屬性或者SplashActivity界面
- 注意 windowSplashscreenContent屬性是在Android8.0(v26)上才有的,如果在之前的版本上適配啟動(dòng)界面,應(yīng)該新增個(gè)Activity,即 SplashActivity。
- 冷啟動(dòng)、熱啟動(dòng)
- Splash Screen:展示品牌Logo或Slogan
- 如果只是單純的顯示個(gè)界面,只需要在themes里設(shè)置<item name="android:windowSplashscreenContent">@color/jetpack_green_500</item>即可。
- Advertisement Screen:展示節(jié)日活動(dòng)或日常廣告
- Guide Screen:演示重點(diǎn)功能,一般只展示一次
- 參考博客:Android 12上全新的應(yīng)用啟動(dòng)API,適配一下?
樣式系統(tǒng)、沉浸式(在Android6.0、8.1、10、11上已經(jīng)適配,詳見demo)
- 本demo沉侵式方案。關(guān)鍵屬性:windowTranslucentStatus、statusBarColor、fitsSystemWindows、mSemiTransparentBarColor、clipToPadding
- 在themes中設(shè)置(或者通過代碼設(shè)置) <item name="android:windowTranslucentStatus">true</item>。調(diào)試發(fā)現(xiàn)不設(shè)置 <item name="android:statusBarColor">@android:color/transparent</item> 也行。
- 在布局中設(shè)置(或者通過代碼設(shè)置) AppBarLayout 的屬性 android:fitsSystemWindows="true",是為了防止AppBarLayout顯示在statusbar上。
- 通過反射設(shè)置 decorView 的 mSemiTransparentBarColor 為透明即可。代碼如下:
try { val decorView = window.decorView::class.java val field = decorView.getDeclaredField("mSemiTransparentBarColor") field.isAccessible = true field.setInt(window.decorView, Color.TRANSPARENT) } catch (e: Exception) { } - 注意:statusbar 和 navigationbar 道理一樣
- 參考文章: