最近项目有个需求是实现类似百度网盘和一刻相册的云盘相册效果,刚好有时间就可以解析一下实现的思路
在开发现代云存储和相册类应用(如百度网盘、一刻相册)中,高效处理在线与本地数据的混合展示,同时实现流畅的分页和动态布局
本文以 Paging3 为核心,结合动态布局和数据混合技术,实现以下目标:
class CloudListPagingSource(private val token: String) : PagingSource<Int, CloudRecord>() {
override fun getRefreshKey(state: PagingState<Int, CloudPhotoRecord>): Int? {
return null
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CloudPhotoRecord> {
return try {
val page = params.key ?: FIRST_PAGE_INDEX
val response = CloudService.getCloudPage(token, page)
val responseData = response.data
if (response.isSuccess && responseData != null) {
val currentPage = responseData.current
val prevKey = if (page > FIRST_PAGE_INDEX) currentPage - 1 else null
val nextKey = if (page < responseData.totalPage) currentPage + 1 else null
val dataList = mutableListOf<CloudPhotoRecord>()
LoadResult.Page(data = dataList, prevKey = prevKey, nextKey = nextKey)
} else {
LoadResult.Error(RuntimeException(response.msg))
}
} catch (t: Throwable) {
Timber.e(t)
LoadResult.Error(t)
}
}
companion object {
private const val FIRST_PAGE_INDEX = 1
}
}
PagingConfig 的参数选择直接影响加载性能:
val pagingFlow = Pager(
config = PagingConfig(
pageSize = 30,
enablePlaceholders = false,
prefetchDistance = 5
),
pagingSourceFactory = { CloudListPagingSource(token) }
).flow.cachedIn(viewModelScope)
优化:针对大卡片视图的场景建议使用小批量(10-20),而对于网格视图,较大的批量(50-100)更高效。
在混合数据场景下,列表需要支持多种布局类型,例如日期标题和内容项。以下是多视图适配的实现:
class CloudPagingAdapter : PagingDataAdapter<MyData, RecyclerView.ViewHolder>(DIFF_CALLBACK) {
override fun getItemViewType(position: Int): Int {
return getItem(position)?.type ?: ViewType.ITEM
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == ViewType.HEADER) {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_header, parent, false)
HeaderViewHolder(view)
} else {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_content, parent, false)
ContentViewHolder(view)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
if (holder is HeaderViewHolder) {
holder.bind(item)
} else if (holder is ContentViewHolder) {
holder.bind(item)
}
}
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<MyData>() {
override fun areItemsTheSame(oldItem: MyData, newItem: MyData): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MyData, newItem: MyData): Boolean {
return oldItem == newItem
}
}
}
}
class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(data: MyData) {
// Bind header-specific data to the view
itemView.findViewById<TextView>(R.id.headerTitle).text = data.content
}
}
class ContentViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(data: MyData) {
// Bind content-specific data to the view
itemView.findViewById<TextView>(R.id.contentText).text = data.content
}
}
aunch(Dispatchers.IO) {
cloudViewModel.getCloudListByPage(token).collectLatest {
cloudPageAdapter.submitData(it)
}
}
根据布局类型动态设置
spanSize:
al layoutManager = GridLayoutManager(context, 4) // 4列网格
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (cloudPageAdapter.getItemViewType(position) == R.layout.item_cloud_photo_title) {
4
} else {
1
}
}
}
binding.rvPhoto.layoutManager = layoutManager
在线与本地数据混合需要解决以下四种情况:
通过时间戳的逻辑对齐,插入中间范围的数据
val localData = mCloudRecordDao.getRecordsByTimeRange(startTime, endTime) cloudList.addAll(localData)
通过时间戳的逻辑对齐,实现本地数据的无缝插入(本地数据时间比在线数据新):
if (page == 1 && index == 0) {
val topTempData = mCloudRecordDao.getRecordsAfter(endTime).sortedByDescending { it.mediaCreateTime }
val topData = mergeLocalData(topTempData, endTime, cloudPage.date)
Timber.d("topData==${preTime}==${startTime}===${topData}")
cloudList.addAll(0, topData)
}
通过时间戳的逻辑对齐,本地数据旧于所有在线数据:
也就是分页最后一页加载时间范围之外的数据
监听加载状态,实现动态切换:
cloudPageAdapter.addLoadStateListener(mLoadStateListener)
private val mLoadStateListener: (CombinedLoadStates) -> Unit = {
when (it.refresh) {
is LoadState.NotLoading -> {
mWaitDialog.dismiss()
Timber.d("mLoadStateListener == LoadState.NotLoading")
if (cloudPageAdapter.isEmpty()) {
binding.ivEmpty.isVisible = true
binding.rvPhoto.isVisible = false
} else {
binding.ivEmpty.isVisible = false
binding.rvPhoto.isVisible = true
}
}
is LoadState.Loading -> {
mWaitDialog.showWaitDialog("加载资源中")
}
is LoadState.Error -> {
Timber.d("mLoadStateListener == LoadState.Error")
if (!canLoadLocal) {
initLocalData()
} else {
mWaitDialog.dismiss()
binding.ivEmpty.isVisible = true
binding.rvPhoto.isVisible = false
}
}
else -> {}
}
}
日期标题插入逻辑是提升用户体验的重要环节。为确保顺序一致,设置标题时间戳为当天的最大值:
val headerTimestamp = DateUtils.getEndOfDayTimestamp(data.date)
cachedIn 避免重复加载;
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69917874/viewspace-3054485/ ,如需转载,请注明出处,否则将追究法律责任。