图片添加加载效果
- 为AsyncImage添加了Shimmer加载效果 - 优化了热门动态的加载逻辑 - 统一了ViewModel的重置方法名
This commit is contained in:
@@ -194,7 +194,7 @@ object AppState {
|
||||
// 重置动态列表页面
|
||||
TimelineMomentViewModel.ResetModel()
|
||||
DynamicViewModel.ResetModel()
|
||||
HotMomentViewModel.ResetModel()
|
||||
HotMomentViewModel.resetModel()
|
||||
|
||||
// 重置我的页面
|
||||
MyProfileViewModel.ResetModel()
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -79,7 +81,8 @@ fun CustomAsyncImage(
|
||||
errorRes: Int? = null,
|
||||
@DrawableRes
|
||||
defaultRes: Int? = null,
|
||||
contentScale: ContentScale = ContentScale.Crop
|
||||
contentScale: ContentScale = ContentScale.Crop,
|
||||
showShimmer: Boolean = true
|
||||
) {
|
||||
val localContext = LocalContext.current
|
||||
|
||||
@@ -91,28 +94,88 @@ fun CustomAsyncImage(
|
||||
// 优先使用 defaultRes,然后是 placeholderRes
|
||||
val fallbackRes = defaultRes ?: placeholderRes
|
||||
if (fallbackRes != null) {
|
||||
if (showShimmer) {
|
||||
SimpleShimmer(modifier = modifier) {
|
||||
Image(
|
||||
painter = androidx.compose.ui.res.painterResource(fallbackRes),
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = contentScale
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Image(
|
||||
painter = androidx.compose.ui.res.painterResource(fallbackRes),
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
contentScale = contentScale
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 Bitmap 类型
|
||||
if (imageUrl is Bitmap) {
|
||||
if (showShimmer) {
|
||||
SimpleShimmer(modifier = modifier) {
|
||||
Image(
|
||||
bitmap = imageUrl.asImageBitmap(),
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = contentScale
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Image(
|
||||
bitmap = imageUrl.asImageBitmap(),
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
contentScale = contentScale
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 处理字符串URL
|
||||
if (showShimmer) {
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context ?: localContext)
|
||||
.data(imageUrl)
|
||||
.crossfade(200)
|
||||
.memoryCachePolicy(coil.request.CachePolicy.ENABLED)
|
||||
.diskCachePolicy(coil.request.CachePolicy.ENABLED)
|
||||
.apply {
|
||||
// 设置占位符图片
|
||||
if (placeholderRes != null) {
|
||||
placeholder(placeholderRes)
|
||||
}
|
||||
// 设置错误时显示的图片
|
||||
if (errorRes != null) {
|
||||
error(errorRes)
|
||||
}
|
||||
}
|
||||
.build(),
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = contentScale,
|
||||
imageLoader = imageLoader,
|
||||
onLoading = { isLoading = true },
|
||||
onSuccess = { isLoading = false },
|
||||
onError = { isLoading = false }
|
||||
)
|
||||
|
||||
// 只在加载时显示shimmer
|
||||
if (isLoading) {
|
||||
SimpleShimmer(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context ?: localContext)
|
||||
.data(imageUrl)
|
||||
@@ -135,6 +198,7 @@ fun CustomAsyncImage(
|
||||
contentScale = contentScale,
|
||||
imageLoader = imageLoader
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.aiosman.ravenow.ui.composables
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* 从左到右扫描的Shimmer加载效果
|
||||
*/
|
||||
@Composable
|
||||
fun ShimmerEffect(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val translateAnim by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1000f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 1200,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
repeatMode = RepeatMode.Restart
|
||||
)
|
||||
)
|
||||
|
||||
val shimmerColors = listOf(
|
||||
Color.LightGray.copy(alpha = 0.6f),
|
||||
Color.LightGray.copy(alpha = 0.2f),
|
||||
Color.LightGray.copy(alpha = 0.6f),
|
||||
)
|
||||
|
||||
val brush = Brush.linearGradient(
|
||||
colors = shimmerColors,
|
||||
start = Offset.Zero,
|
||||
end = Offset(x = translateAnim, y = translateAnim)
|
||||
)
|
||||
|
||||
Box(modifier = modifier) {
|
||||
content()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(brush)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带圆角的Shimmer占位符
|
||||
*/
|
||||
@Composable
|
||||
fun ShimmerPlaceholder(
|
||||
modifier: Modifier = Modifier,
|
||||
cornerRadius: Float = 8f
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = Color.LightGray.copy(alpha = 0.3f),
|
||||
shape = RoundedCornerShape(cornerRadius.dp)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.aiosman.ravenow.ui.composables
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* 简单的从左到右扫描的Shimmer加载效果
|
||||
*/
|
||||
@Composable
|
||||
fun SimpleShimmer(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val translateAnim by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1000f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 1200,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
repeatMode = RepeatMode.Restart
|
||||
)
|
||||
)
|
||||
|
||||
val shimmerColors = listOf(
|
||||
Color.LightGray.copy(alpha = 0.6f),
|
||||
Color.LightGray.copy(alpha = 0.2f),
|
||||
Color.LightGray.copy(alpha = 0.6f),
|
||||
)
|
||||
|
||||
val brush = Brush.linearGradient(
|
||||
colors = shimmerColors,
|
||||
start = Offset.Zero,
|
||||
end = Offset(x = translateAnim, y = translateAnim)
|
||||
)
|
||||
|
||||
Box(modifier = modifier) {
|
||||
content()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(brush)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带圆角的Shimmer占位符
|
||||
*/
|
||||
@Composable
|
||||
fun SimpleShimmerPlaceholder(
|
||||
modifier: Modifier = Modifier,
|
||||
cornerRadius: Float = 8f
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = Color.LightGray.copy(alpha = 0.3f),
|
||||
shape = RoundedCornerShape(cornerRadius.dp)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -2,51 +2,86 @@ package com.aiosman.ravenow.ui.index.tabs.moment.tabs.hot
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.aiosman.ravenow.AppState
|
||||
import com.aiosman.ravenow.data.MomentService
|
||||
import com.aiosman.ravenow.entity.MomentEntity
|
||||
import com.aiosman.ravenow.entity.MomentLoaderExtraArgs
|
||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||
import com.aiosman.ravenow.ui.index.tabs.moment.BaseMomentModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
|
||||
object HotMomentViewModel : ViewModel() {
|
||||
private val momentService: MomentService = MomentServiceImpl()
|
||||
private val _discoverMomentsFlow =
|
||||
MutableStateFlow<PagingData<MomentEntity>>(PagingData.empty())
|
||||
val discoverMomentsFlow = _discoverMomentsFlow.asStateFlow()
|
||||
var firstLoad = true
|
||||
fun refreshPager() {
|
||||
if (!firstLoad) {
|
||||
return
|
||||
}
|
||||
firstLoad = false
|
||||
private val _discoverMoments = MutableStateFlow<List<MomentEntity>>(emptyList())
|
||||
val discoverMoments = _discoverMoments.asStateFlow()
|
||||
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
private val _isRefreshing = MutableStateFlow(false)
|
||||
val isRefreshing = _isRefreshing.asStateFlow()
|
||||
|
||||
private var currentPage = 1
|
||||
private var hasMoreData = true
|
||||
private val pageSize = 20
|
||||
|
||||
fun loadMoments() {
|
||||
if (_isLoading.value || !hasMoreData) return
|
||||
|
||||
viewModelScope.launch {
|
||||
Pager(
|
||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
MomentPagingSource(
|
||||
MomentRemoteDataSource(momentService),
|
||||
_isLoading.value = true
|
||||
try {
|
||||
val response = momentService.getMoments(
|
||||
pageNumber = currentPage,
|
||||
trend = true
|
||||
)
|
||||
|
||||
if (response.list.isNotEmpty()) {
|
||||
if (currentPage == 1) {
|
||||
_discoverMoments.value = response.list
|
||||
} else {
|
||||
_discoverMoments.value = _discoverMoments.value + response.list
|
||||
}
|
||||
).flow.cachedIn(viewModelScope).collectLatest {
|
||||
_discoverMomentsFlow.value = it
|
||||
currentPage++
|
||||
hasMoreData = response.list.size >= response.pageSize
|
||||
} else {
|
||||
hasMoreData = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 处理错误
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
fun ResetModel(){
|
||||
firstLoad = true
|
||||
|
||||
fun refreshMoments() {
|
||||
viewModelScope.launch {
|
||||
_isRefreshing.value = true
|
||||
currentPage = 1
|
||||
hasMoreData = true
|
||||
try {
|
||||
val response = momentService.getMoments(
|
||||
pageNumber = 1,
|
||||
trend = true
|
||||
)
|
||||
_discoverMoments.value = response.list
|
||||
currentPage = 2
|
||||
hasMoreData = response.list.size >= response.pageSize
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
_isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetModel() {
|
||||
currentPage = 1
|
||||
hasMoreData = true
|
||||
_discoverMoments.value = emptyList()
|
||||
_isLoading.value = false
|
||||
_isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
@@ -29,13 +31,15 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import com.aiosman.ravenow.LocalAppTheme
|
||||
import com.aiosman.ravenow.LocalNavController
|
||||
import com.aiosman.ravenow.R
|
||||
@@ -57,12 +61,14 @@ fun HotMomentsList() {
|
||||
val navController = LocalNavController.current
|
||||
val navigationBarPaddings =
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 48.dp
|
||||
val isRefreshing by model.isRefreshing.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
model.refreshPager()
|
||||
model.loadMoments()
|
||||
}
|
||||
var refreshing by remember { mutableStateOf(false) }
|
||||
val state = rememberPullRefreshState(refreshing, onRefresh = {
|
||||
model.refreshPager()
|
||||
|
||||
val state = rememberPullRefreshState(isRefreshing, onRefresh = {
|
||||
model.refreshMoments()
|
||||
})
|
||||
|
||||
Column(
|
||||
@@ -84,7 +90,7 @@ fun HotMomentsList() {
|
||||
.padding(2.dp)
|
||||
) {
|
||||
DiscoverView()
|
||||
PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
|
||||
PullRefreshIndicator(isRefreshing, state, Modifier.align(Alignment.TopCenter))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,17 +99,37 @@ fun HotMomentsList() {
|
||||
@Composable
|
||||
fun DiscoverView() {
|
||||
val model = HotMomentViewModel
|
||||
var dataFlow = model.discoverMomentsFlow
|
||||
var moments = dataFlow.collectAsLazyPagingItems()
|
||||
val moments by model.discoverMoments.collectAsState()
|
||||
val isLoading by model.isLoading.collectAsState()
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
val gridState = rememberLazyGridState()
|
||||
|
||||
// 监听滚动到底部,自动加载更多
|
||||
LaunchedEffect(gridState, moments.size) {
|
||||
snapshotFlow {
|
||||
val layoutInfo = gridState.layoutInfo
|
||||
val visibleItemsInfo = layoutInfo.visibleItemsInfo
|
||||
if (visibleItemsInfo.isNotEmpty() && moments.isNotEmpty()) {
|
||||
val lastVisibleItemIndex = visibleItemsInfo.lastOrNull()?.index ?: 0
|
||||
lastVisibleItemIndex >= moments.size - 6 // 距离底部还有6个项目时开始加载
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}.collectLatest { shouldLoadMore ->
|
||||
if (shouldLoadMore && !isLoading) {
|
||||
model.loadMoments()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
state = gridState,
|
||||
modifier = Modifier.fillMaxSize().padding(bottom = 8.dp),
|
||||
// contentPadding = PaddingValues(8.dp)
|
||||
) {
|
||||
items(moments.itemCount) { idx ->
|
||||
val momentItem = moments[idx] ?: return@items
|
||||
items(moments) { momentItem ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -122,7 +148,8 @@ fun DiscoverView() {
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
context = context
|
||||
context = context,
|
||||
showShimmer = true
|
||||
)
|
||||
if (momentItem.images.size > 1) {
|
||||
Box(
|
||||
|
||||
@@ -80,7 +80,7 @@ object ResourceCleanupManager {
|
||||
// 重置动态相关ViewModel
|
||||
TimelineMomentViewModel.ResetModel()
|
||||
DynamicViewModel.ResetModel()
|
||||
HotMomentViewModel.ResetModel()
|
||||
HotMomentViewModel.resetModel()
|
||||
|
||||
// 重置个人资料相关ViewModel
|
||||
MyProfileViewModel.ResetModel()
|
||||
@@ -233,7 +233,7 @@ object ResourceCleanupManager {
|
||||
"moment" -> {
|
||||
TimelineMomentViewModel.ResetModel()
|
||||
DynamicViewModel.ResetModel()
|
||||
HotMomentViewModel.ResetModel()
|
||||
HotMomentViewModel.resetModel()
|
||||
}
|
||||
"profile" -> {
|
||||
MyProfileViewModel.ResetModel()
|
||||
|
||||
Reference in New Issue
Block a user