前面的教程我們遺留了一個(gè)問(wèn)題:我們的列表只能請(qǐng)求第一頁(yè),本節(jié)我們將實(shí)現(xiàn)分頁(yè)加載的效果和下拉刷新的效果。
本節(jié)內(nèi)容您將學(xué)習(xí)到如下內(nèi)容:
- 用Paging庫(kù)實(shí)現(xiàn)加載更多
- 用Paging庫(kù)和SwipeRefreshLayout結(jié)合實(shí)現(xiàn)下拉刷新
- 給RecyclerView添加Footer
- 加載失敗進(jìn)行重試
- Android幀動(dòng)畫(huà)的實(shí)現(xiàn)方式
Paging的優(yōu)勢(shì)
Paging庫(kù)之前,我們進(jìn)行分頁(yè)加載使用的方法是監(jiān)聽(tīng)RecyclerView的滾動(dòng)事件,當(dāng)快滾動(dòng)到底部的時(shí)候進(jìn)行新數(shù)據(jù)的請(qǐng)求。
這個(gè)方法有一定的問(wèn)題,譬如當(dāng)用戶(hù)在接近底部的時(shí)候快速上下移動(dòng),有可能會(huì)有多次請(qǐng)求發(fā)出,如果處理不當(dāng),就有可能漏掉數(shù)據(jù)或者產(chǎn)生重復(fù)數(shù)據(jù)。
Google引入的Paging,它抽象出來(lái)一些自動(dòng)加載的邏輯類(lèi),我們?cè)谶@里邏輯類(lèi)里面填入所需要的內(nèi)容,然后自動(dòng)分頁(yè)加載的過(guò)程就由Paging庫(kù)自動(dòng)給我們完成了。
Paging實(shí)現(xiàn)分頁(yè)加載更多
- 首先需要引入Paging依賴(lài)庫(kù)
// 添加依賴(lài)
def paging_version = '2.1.2'
implementation "androidx.paging:paging-runtime:$paging_version"
- 選擇合適的DataSource
DataSource就是數(shù)據(jù)源,顧名思義就是列表數(shù)據(jù)從這個(gè)類(lèi)里面獲取得到。
Paging提供有三種DataSource
- ItemKeyedDataSource - 使用場(chǎng)景:通過(guò)ID請(qǐng)求這個(gè)ID后面的數(shù)據(jù)
- PageKeyedDataSource - 使用場(chǎng)景:通過(guò)Page請(qǐng)求下一個(gè)Page的數(shù)據(jù)
- PositionalDataSource - 使用場(chǎng)景:請(qǐng)求從第X條到第Y條的數(shù)據(jù)
通過(guò)上面的介紹,我們已經(jīng)確定我們需要的是PageKeyedDataSource。
新建PlaylistDataSource繼承自PageKeyedDataSource
class PlaylistDataSource : PageKeyedDataSource<Int, PlayItem>() {
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, PlayItem>
) {
TODO("Not yet implemented")
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, PlayItem>) {
TODO("Not yet implemented")
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, PlayItem>) {
TODO("Not yet implemented")
}
}
新建的這個(gè)類(lèi)構(gòu)造函數(shù)有一個(gè)泛型<Int, PlayItem>, Int是分頁(yè)的時(shí)候傳入的頁(yè)數(shù),PlayItem是每個(gè)Item對(duì)應(yīng)的數(shù)據(jù)模型。
初始化的時(shí)候需要復(fù)寫(xiě)三方方法:
-
loadInitial是最開(kāi)始加載的時(shí)候調(diào)用的數(shù)據(jù)請(qǐng)求方法 -
loadBefore是頁(yè)面向上滾動(dòng)的時(shí)候時(shí)候調(diào)用數(shù)據(jù)請(qǐng)求的方法 -
loadAfter是頁(yè)面向上滾動(dòng)的時(shí)候時(shí)候調(diào)用的數(shù)據(jù)請(qǐng)求方法
- 改造DataSource
我們知道了這三方復(fù)寫(xiě)方法的含義后,我們修改下代碼:
// 1
class PlaylistDataSource(private val type: String, private val scope: CoroutineScope) : PageKeyedDataSource<Int, PlayItem>() {
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, PlayItem>
) {
scope.launch {
try {
when (type) {
"推薦" -> {
// 2
val response = PlaylistRepository.getRecommendPlaylist(params.requestedLoadSize, 0)
// 3
callback.onResult(response.playlists, -1, 1)
}
"精品" -> {
val response = PlaylistRepository.getHighQualityPlaylist(params.requestedLoadSize, 0)
callback.onResult(response.playlists, -1, 1)
}
"官方" -> {
val response = PlaylistRepository.getOrgPlaylist(params.requestedLoadSize, 0)
callback.onResult(response.playlists, -1, 1)
}
else -> {
val response = PlaylistRepository.getPlaylistByCat(params.requestedLoadSize, 0, type)
callback.onResult(response.playlists, -1, 1)
}
}
} catch (e: Exception) {
Log.d("PlaylistDataSource", "$e")
}
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, PlayItem>) {
scope.launch {
try {
when (type) {
"推薦" -> {
// 4
val response = PlaylistRepository.getRecommendPlaylist(params.requestedLoadSize, params.key)
// 5
callback.onResult(response.playlists, params.key + 1)
}
"精品" -> {
val response = PlaylistRepository.getHighQualityPlaylist(params.requestedLoadSize, params.key)
callback.onResult(response.playlists, params.key + 1)
}
"官方" -> {
val response = PlaylistRepository.getOrgPlaylist(params.requestedLoadSize, params.key)
callback.onResult(response.playlists, params.key + 1)
}
else -> {
val response = PlaylistRepository.getPlaylistByCat(params.requestedLoadSize, params.key, type)
callback.onResult(response.playlists, params.key + 1)
}
}
} catch (e: Exception) {
Log.d("PlaylistDataSource", "$e")
}
}
}
// 6
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, PlayItem>) {
TODO("Not yet implemented")
}
}
一步步解釋下代碼的含義:
- 構(gòu)造函數(shù)添加了兩個(gè)變量
type和scope,分別為歌單類(lèi)型和協(xié)程作用域 -
params.requestedLoadSize是PlaylistDataSource初始化的時(shí)候配置的,代表每頁(yè)請(qǐng)求多少個(gè)Item,項(xiàng)目中配置的是15。如何配置后續(xù)介紹。 -
callback.onResult是通過(guò)回調(diào)將結(jié)果返回,這個(gè)方法有三個(gè)參數(shù),第一個(gè)參數(shù)response.playlists是數(shù)據(jù)結(jié)果,第二個(gè)參數(shù)-1是請(qǐng)求上一頁(yè)需要傳入的頁(yè)的數(shù)值(我們的項(xiàng)目中這個(gè)值沒(méi)有實(shí)際意義),第三個(gè)參數(shù)1是請(qǐng)求下一頁(yè)需要傳入的頁(yè)的數(shù)值
提示:1 這個(gè)數(shù)值會(huì)通過(guò)Paging傳給
loadAfter方法中的params: LoadParams<Int>這個(gè)參數(shù)
-
params.requestedLoadSize和步驟2中的意義相同,params.key就是上面callback.onResult傳的值1 -
callback.onResult(response.playlists, params.key + 1)中的params.key + 1就是將頁(yè)面數(shù)值設(shè)置成當(dāng)前的數(shù)值+1 -
loadBefore我們用不到,所以可以不用覆寫(xiě)方法
- DataSource.Factory
DataSource一般由DataSource.Factory來(lái)初始化。
class PlaylistDataSourceFactory(private val type: String, private val scope: CoroutineScope) : DataSource.Factory<Int, PlayItem>() {
override fun create(): DataSource<Int, PlayItem> {
return PlaylistDataSource(type, scope)
}
}
DataSource.Factory 需要覆寫(xiě)create方法,返回一個(gè)DataSource對(duì)象就可以了。
- 改造Adapter 為PagedListAdapter
使用Paging功能需要將PlaylistItemAdapter繼承由ListAdapter改為PagedListAdapter。這樣就可以了,因?yàn)?strong>PagedListAdapter中實(shí)現(xiàn)了對(duì)Paging的支持。
class PlaylistItemAdapter:
PagedListAdapter<PlayItem, PlaylistItemAdapter.PlaylistItemHolder>(DiffCallback) {
...
}
問(wèn)題:可否不使用DataSource.Factory來(lái)創(chuàng)建DataSource對(duì)象?
- 修改PlayListViewModel
由于網(wǎng)絡(luò)請(qǐng)求移到了DataSource,ViewModel的代碼就大大精簡(jiǎn)了。只留下一個(gè)變量。
class PlayListViewModel(private val type: String) : ViewModel() {
var pagedlistLiveData = LivePagedListBuilder<Int, PlayItem>(
PlaylistDataSourceFactory(type, viewModelScope),
PagedList.Config.Builder().setPageSize(15).build()
).build()
}
這段代碼比較長(zhǎng),我們分布解釋下:
-
LivePagedListBuilder有兩個(gè)參數(shù),第一個(gè)參數(shù)就是DataSource對(duì)象,這里是通過(guò)上面創(chuàng)建的工廠(chǎng)方法創(chuàng)建的。這里要求傳入的是DataSource.Factory -
PagedList.Config.Builder().setPageSize(15).build()這個(gè)setPageSize(15)代表的是每頁(yè)請(qǐng)求15條數(shù)據(jù)。當(dāng)然PagedList.Config還可以進(jìn)行其他一些配置。 -
LivePagedListBuilder通過(guò)build方法返回的是一個(gè)LiveData
public LiveData<PagedList<Value>> build() {
...
}
問(wèn)題1:什么是PagedList?
PagedList是一個(gè)改造后的List,當(dāng)用戶(hù)滑動(dòng)列表接近底部的時(shí)候就會(huì)委托DataSource去請(qǐng)求新的數(shù)據(jù)。
問(wèn)題2:為什么需要用LiveData包裝PagedList?
首先LiveData包裝PagedList可以使其能被觀察,這樣就能實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)UI的重繪;
再次,用戶(hù)進(jìn)行下拉刷新的時(shí)候通過(guò)只需要調(diào)用invalidate方法,****LiveData****會(huì)重新生成一個(gè)新的PagedList,這個(gè)PagedList會(huì)委托DataSource去請(qǐng)求新的數(shù)據(jù) 這樣所有的流程就又可以重新開(kāi)始自動(dòng)進(jìn)行了。
- 修改Fragment
var viewModel = ViewModelProviders.of(this, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(PlayListViewModel::class.java)) {
return PlayListViewModel(it) as T
}
throw IllegalArgumentException(" unKnown ViewModel class ")
}
}).get(PlayListViewModel::class.java)
由于需要初始ViewModel的時(shí)候需要傳參,這里修改了ViewModel的初始化方法,重寫(xiě)ViewModelProvider.Factory的create方法。
監(jiān)聽(tīng)LiveData
viewModel.pagedlistLiveData.observe(viewLifecycleOwner, Observer {
playAdapter.submitList(it)
})
到目前為止,加載更多的功能就實(shí)現(xiàn)了。

Paging和SwipRefreshLayout組合實(shí)現(xiàn)下拉刷新
- 實(shí)現(xiàn)下來(lái)刷新需要修改下布局,將根布局設(shè)置成SwipeRefreshLayout。
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/refreshlayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".Fragment.PlayListFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraint_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
- 添加重新開(kāi)始請(qǐng)求的方法
/* 下拉刷新 */
fun resetQuery() {
pagedlistLiveData.value?.dataSource?.invalidate()
}
這個(gè)方法在前面有解釋?zhuān)筒辉儋樖隽恕?/p>
- 下拉監(jiān)聽(tīng)
viewModel.pagedlistLiveData.observe(viewLifecycleOwner, Observer {
playAdapter.submitList(it)
// 1
refreshlayout.isRefreshing = false
})
// 2
refreshlayout.setOnRefreshListener {
viewModel.resetQuery()
}
- 刷新完成后,
isRefreshing置為false, 這時(shí)候刷新動(dòng)畫(huà)會(huì)取消 - 監(jiān)聽(tīng)下拉執(zhí)行刷新

給RecyclerView添加加載狀態(tài)的Footer
細(xì)心的你可能會(huì)發(fā)現(xiàn)當(dāng)RecyclerView滑到底部的時(shí)候可以實(shí)現(xiàn)自動(dòng)加載更多,但是會(huì)有小小的卡頓,特別是網(wǎng)絡(luò)不太好的時(shí)候,因?yàn)榫W(wǎng)絡(luò)請(qǐng)求是需要加載時(shí)間的。
為了良好的用戶(hù)體驗(yàn),可以加載過(guò)程中需要添加一個(gè)Footer,給用戶(hù)一個(gè)正在加載的反饋。此外也可以通過(guò)修改Footer的文案,當(dāng)加載出現(xiàn)錯(cuò)誤或者所有數(shù)據(jù)都加載完后給用戶(hù)一個(gè)提示。
示例如下:



點(diǎn)擊重試可以重新加載請(qǐng)求失敗的頁(yè)的數(shù)據(jù)
- 首先建一個(gè)Footer而布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center">
<ImageView
android:id="@+id/loading_iv"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="10dp"
android:layout_weight="0"
android:background="@drawable/loading_list" />
<TextView
android:id="@+id/loading_tv"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="0"
android:gravity="center_vertical"
android:text="加載中..."
android:textColor="#9E9E9E"
android:textSize="16sp" />
</LinearLayout>
footer比較簡(jiǎn)單,就是有一個(gè)圖片loading_iv和文本loading_tv
- 由于這個(gè)Footer是放在Recyclervie中,所以需要建立一個(gè)LoadingViewHolder
class LoadingViewHolder(v: View) : RecyclerView.ViewHolder(v) {
companion object {
// 1
fun instance(parent: ViewGroup): LoadingViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.loading_layout, parent, false)
return LoadingViewHolder(v)
}
}
// 2
fun bindNetWorkStatus(loadingStatus: LoadingStatus?) {
// 3
when(loadingStatus) {
LoadingStatus.Failed -> {
itemView.loading_tv.text = "點(diǎn)擊重試"
itemView.loading_iv.visibility = View.GONE
itemView.isClickable = true
}
LoadingStatus.Completed -> {
itemView.loading_tv.text = "加載完畢"
itemView.loading_iv.visibility = View.GONE
itemView.isClickable = false
}
LoadingStatus.Loading -> {
itemView.loading_tv.text = "加載中..."
itemView.loading_iv.visibility = View.VISIBLE
itemView.isClickable = false
}
}
}
}
代碼解釋如下:
- 創(chuàng)建了一個(gè)類(lèi)方法
instance,加載布局文件,初始化LoadingViewHolder。 -
bindNetWorkStatus根據(jù)不同的LoadingStatus展示不同的樣式,LoadingStatus.Failed時(shí)候可以點(diǎn)擊重試
加載狀態(tài)的枚舉定義如下:
// 加載的狀態(tài)
enum class LoadingStatus {
InitalLoading, // 初次加載
Loading, // 正在加載
Failed, // 加載失敗
Completed // 數(shù)據(jù)全部加載完
}
- 改造Adapter
先定義一個(gè)是否顯示Footer的變量并且添加覆寫(xiě)兩個(gè)方法:
class PlaylistItemAdapter(private val viewModel: PlayListViewModel):
PagedListAdapter<PlayItem, RecyclerView.ViewHolder>(DiffCallback) {
// 1
private var hasLoadingFooter = false
// 2
override fun getItemCount(): Int {
return super.getItemCount() + if (hasLoadingFooter) 1 else 0
}
// 3
override fun getItemViewType(position: Int): Int {
return if (hasLoadingFooter && position == itemCount - 1) R.layout.loading_layout else R.layout.item_playlist
}
}
代碼解釋如下:
- 定義一個(gè)
hasLoadingFooter的變量控制是否顯示Footer,第一次加載的時(shí)候不顯示Footer,因?yàn)槲覀円呀?jīng)有下拉刷新了。 -
getItemCount是返回顯示多少I(mǎi)tem,hasLoadingFooter為真的時(shí)候得比Item多加一行, -
getItemViewType是返回每個(gè)Item對(duì)應(yīng)的布局文件, 因?yàn)椴煌腎tem顯示的樣式不一樣,需要通過(guò)這個(gè)方法指定
修改兩個(gè)覆寫(xiě)方法
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
return when(viewType) {
R.layout.item_playlist -> {
PlaylistItemHolder.instance(parent)
}
else -> {
LoadingViewHolder.instance(parent)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(holder.itemViewType) {
R.layout.loading_layout -> {
(holder as LoadingViewHolder).bindNetWorkStatus(_loadingStatus)
}
else -> {
getItem(position)?.let {
(holder as PlaylistItemHolder).bindPlayItem(it)
}
}
}
}
-
onCreateViewHolder根據(jù)不同的viewType返回不同的ViewHolder -
onBindViewHolder根據(jù)不同的itemViewType進(jìn)行不同的綁定
目前為止Adapter準(zhǔn)備好了,也就是說(shuō)UI層面的邏輯好了,那現(xiàn)在就需要有一個(gè)加載狀態(tài)的觸發(fā)了。很明顯加載狀態(tài)觸發(fā)的位置是DataSource。
疑問(wèn): DataSource被ViewModel持有,如何反向傳遞數(shù)據(jù)呢? 上節(jié)有介紹可以有CallBack和LiveData等形式。
采取LiveData反向傳遞如何實(shí)現(xiàn)呢?
- 實(shí)現(xiàn)Datasource回傳LoadingStatus給ViewModel
實(shí)現(xiàn)邏輯是ViewModel 定義一個(gè)LiveData,層層傳遞給Datasource。
Datasource持有這個(gè)LiveData,就可以修改值了。
<!-- PlayListViewModel -->
class PlayListViewModel(type: String) : ViewModel() {
// 1
var loadingStatusLiveData: LiveData<LoadingStatus> = _loadingStatusLiveData
// 2
var pagedListLiveData = LivePagedListBuilder<Int, PlayItem>(
PlaylistDataSourceFactory(type, viewModelScope, _loadingStatusLiveData),
PagedList.Config.Builder().setPageSize(15).build()
).build()
}
<!-- PlaylistDataSourceFactory -->
class PlaylistDataSourceFactory(private val type: String, private val scope: CoroutineScope, private val loadingStatusLiveData: MutableLiveData<LoadingStatus>) : DataSource.Factory<Int, PlayItem>() {
override fun create(): DataSource<Int, PlayItem> {
return PlaylistDataSource(type, scope, loadingStatusLiveData)
}
}
<!-- PlaylistDataSource -->
// 1
class PlaylistDataSource(private val type: String, private val scope: CoroutineScope, private val loadingStatusLiveData: MutableLiveData<LoadingStatus>) : PageKeyedDataSource<Int, PlayItem>() {
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, PlayItem>
) {
// 2
loadingStatusLiveData.postValue(LoadingStatus.InitalLoading)
scope.launch {
try {
...
} catch (e: Exception) {
// 2
loadingStatusLiveData.postValue(LoadingStatus.Failed)
}
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, PlayItem>) {
// 2
loadingStatusLiveData.postValue(LoadingStatus.Loading)
scope.launch {
try {
...
} catch (e: Exception) {
// 2
loadingStatusLiveData.postValue(LoadingStatus.Failed)
}
}
}
}
- Adapter監(jiān)聽(tīng)LoadingStatus的變化
<!-- PlayListFragment -->
viewModel.loadingStatusLiveData.observe(viewLifecycleOwner, Observer {
playAdapter.updateLoadingStatus(it)
})
<!-- PlaylistItemAdapter -->
// 更新加載狀態(tài)
fun updateLoadingStatus(loadingStatus: LoadingStatus) {
_loadingStatus = loadingStatus
if (loadingStatus == LoadingStatus.InitalLoading) {
hideLoading()
} else {
showLoading()
}
}
private fun hideLoading() {
if (hasLoadingFooter) {
notifyItemRemoved(itemCount - 1)
}
hasLoadingFooter = false
}
private fun showLoading() {
if (hasLoadingFooter) {
notifyItemChanged(itemCount - 1)
} else {
hasLoadingFooter = true
notifyItemInserted(itemCount - 1)
}
}
這幾個(gè)方法的意義比較簡(jiǎn)單,就是LoadingStatus改變后刷新RecyclerView,及Footer的顯示和隱藏。
一個(gè)小的功能寫(xiě)了不少代碼,主要是流程比較的長(zhǎng),但是由于分層,邏輯確是很清晰。

遺留問(wèn)題,由于GridLayoutManager,是每行三列,所以Footer也只有三分之一寬度。需要改成全屏,覆寫(xiě)onAttachedToRecyclerView方法:
// 這個(gè)方法解決Footer 全屏
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
var layoutManager:RecyclerView.LayoutManager = recyclerView.layoutManager!!
if (layoutManager is GridLayoutManager) {
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup(){
override fun getSpanSize(position: Int): Int {
return if (getItemViewType(position) == R.layout.loading_layout) {
layoutManager.spanCount // Footer時(shí)返回三個(gè)的單元格,從而占據(jù)整個(gè)一行的寬度
} else {
1 // 正常情況下返回一個(gè)單元格
}
}
}
}
}
發(fā)生網(wǎng)絡(luò)錯(cuò)誤后重試
- 用一個(gè)函數(shù)隊(duì)形保留錯(cuò)誤現(xiàn)場(chǎng)
public var retryFun: (() -> Any)? = null
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, PlayItem>
) {
...
// 1
retryFun = null
scope.launch {
try {
} catch (e: Exception) {
...
// 2
retryFun = {loadInitial(params, callback)}
}
}
}
override fun loadAfter(params: LoadParams<Int>, callback:LoadCallback<Int, PlayItem>) {
// 1
retryFun = null
scope.launch {
try {
...
} catch (e: Exception) {
...
// 2
retryFun = { loadAfter(params, callback) }
}
}
}
這段代碼的意思是:
定義retryFun變量,如果發(fā)生錯(cuò)誤就把調(diào)用的方法和參數(shù)賦值給retryFun記錄下來(lái)。
-
ViewModel中定義
retryFun函數(shù)
// 重新嘗試
fun retry() {
(pagedListLiveData.value?.dataSource as PlaylistDataSource).let {
it.retryFun?.invoke()
}
}
- 在Adapter中調(diào)用
retryFun函數(shù)
LoadingViewHolder.instance(parent).also {
it.itemView.setOnClickListener {
viewModel.retry()
}
}
Adapter和ViewModel是獨(dú)立的,所以可以把ViewModel傳入Adapter。
PlaylistItemAdapter(private val viewModel: PlayListViewModel)
這樣整個(gè)流程也就完成了。

最后的效果如下所示:

幀動(dòng)畫(huà)
Footer有一個(gè)幀動(dòng)畫(huà),由于我在本機(jī)網(wǎng)絡(luò)加載較快,所以可能不太明顯。效果如

接下來(lái)我們就實(shí)現(xiàn)下這個(gè)幀動(dòng)畫(huà)的效果
在Drawable文件中加入四個(gè)圖片,這四個(gè)圖片將用來(lái)輪流顯示
icn_loading1,icn_loading2,icn_loading3,icn_loading4在Drawable文件創(chuàng)建一個(gè)loading_list.xml文件, 代碼如下
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/icn_loading1" android:duration="150" />
<item android:drawable="@drawable/icn_loading2" android:duration="150" />
<item android:drawable="@drawable/icn_loading3" android:duration="150" />
<item android:drawable="@drawable/icn_loading4" android:duration="150" />
</animation-list>
- 將這個(gè)Drawable文件作為ImageView的背景
<ImageView
android:id="@+id/loading_iv"
...
android:background="@drawable/loading_list" />
- 代碼中開(kāi)始動(dòng)畫(huà)和結(jié)束動(dòng)畫(huà)
private fun startAnimation() {
val drawable = itemView.loading_iv.background as? AnimationDrawable
drawable?.let {
if (!it.isRunning) it.start()
}
}
fun stopAnimation() {
val drawable = itemView.loading_iv.background as? AnimationDrawable
drawable?.let {
if (it.isRunning) it.stop()
}
}
通過(guò)這幾步,這個(gè)加載動(dòng)畫(huà)就實(shí)現(xiàn)了。