optimize picture show
This commit is contained in:
+1
@@ -52,6 +52,7 @@ fun AlbumGridItem(
|
|||||||
AlbumCoverImage(
|
AlbumCoverImage(
|
||||||
album = album,
|
album = album,
|
||||||
radius = 12.dp,
|
radius = 12.dp,
|
||||||
|
decodeToDisplaySize = true,
|
||||||
modifier = Modifier.fillMaxSize().then(heroMod),
|
modifier = Modifier.fillMaxSize().then(heroMod),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -51,6 +51,7 @@ fun ArtistListItem(
|
|||||||
ArtistPhotoImage(
|
ArtistPhotoImage(
|
||||||
artist = artist,
|
artist = artist,
|
||||||
size = ListLeadingCoverSize,
|
size = ListLeadingCoverSize,
|
||||||
|
decodeToDisplaySize = true,
|
||||||
modifier = Modifier.fillMaxSize().then(heroMod),
|
modifier = Modifier.fillMaxSize().then(heroMod),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -255,7 +255,12 @@ private fun AddSheetMenuRow(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (coverMusic != null) {
|
if (coverMusic != null) {
|
||||||
MusicCoverImage(music = coverMusic, size = 48.dp, radius = 12.dp)
|
MusicCoverImage(
|
||||||
|
music = coverMusic,
|
||||||
|
size = 48.dp,
|
||||||
|
radius = 12.dp,
|
||||||
|
decodeToDisplaySize = true,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
CoverPlaceholder(label = menu.name.take(1), size = 48.dp, radius = 12.dp)
|
CoverPlaceholder(label = menu.name.take(1), size = 48.dp, radius = 12.dp)
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-12
@@ -2,6 +2,7 @@ package com.zhushenwudi.lovelivemusicplayer.ui.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -17,8 +18,10 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.min
|
||||||
import coil3.compose.AsyncImagePainter.State
|
import coil3.compose.AsyncImagePainter.State
|
||||||
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
import org.jetbrains.compose.resources.ExperimentalResourceApi
|
||||||
import com.zhushenwudi.lovelivemusicplayer.AppContainer
|
import com.zhushenwudi.lovelivemusicplayer.AppContainer
|
||||||
@@ -29,7 +32,9 @@ import com.zhushenwudi.lovelivemusicplayer.domain.Music
|
|||||||
import com.zhushenwudi.lovelivemusicplayer.generated.resources.Res
|
import com.zhushenwudi.lovelivemusicplayer.generated.resources.Res
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.image.CoverImageHolder
|
import com.zhushenwudi.lovelivemusicplayer.ui.image.CoverImageHolder
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.image.LlmpAsyncImage
|
import com.zhushenwudi.lovelivemusicplayer.ui.image.LlmpAsyncImage
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.ui.image.decodeSizeBucketPx
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.image.rememberCoverPainter
|
import com.zhushenwudi.lovelivemusicplayer.ui.image.rememberCoverPainter
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.ui.image.sizedMemoryCacheKey
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.image.toImageCacheKey
|
import com.zhushenwudi.lovelivemusicplayer.ui.image.toImageCacheKey
|
||||||
|
|
||||||
private const val FALLBACK_LOGO = "drawable/logo_svg_logo.svg"
|
private const val FALLBACK_LOGO = "drawable/logo_svg_logo.svg"
|
||||||
@@ -49,7 +54,12 @@ fun rememberMusicCoverLocalPath(music: Music?): String? =
|
|||||||
?.takeIf { path -> AppContainer.fileSystem.exists(path) }
|
?.takeIf { path -> AppContainer.fileSystem.exists(path) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 封面可解析且可加载时显示,否则显示默认 Logo。 */
|
/**
|
||||||
|
* 封面可解析且可加载时显示,否则显示默认 Logo。
|
||||||
|
*
|
||||||
|
* @param decodeToDisplaySize 仅列表缩略图开启:按 [size] 解码以省内存/CPU。
|
||||||
|
* 播放页/大图保持默认 false。
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalResourceApi::class)
|
@OptIn(ExperimentalResourceApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MusicCoverImage(
|
fun MusicCoverImage(
|
||||||
@@ -57,11 +67,13 @@ fun MusicCoverImage(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
size: Dp = 48.dp,
|
size: Dp = 48.dp,
|
||||||
radius: Dp = 12.dp,
|
radius: Dp = 12.dp,
|
||||||
|
decodeToDisplaySize: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val uri = rememberMusicCoverUri(music)
|
val uri = rememberMusicCoverUri(music)
|
||||||
CoverAsyncImage(
|
CoverAsyncImage(
|
||||||
modelUri = uri,
|
modelUri = uri,
|
||||||
stableCacheKey = music.musicId?.let { musicCoverCacheKey(it) },
|
stableCacheKey = music.musicId?.let { musicCoverCacheKey(it) },
|
||||||
|
decodeSize = size.takeIf { decodeToDisplaySize },
|
||||||
// 先设置尺寸,再应用调用方修饰符(如 sharedElement 在 size 之后)。
|
// 先设置尺寸,再应用调用方修饰符(如 sharedElement 在 size 之后)。
|
||||||
modifier = Modifier.size(size).then(modifier),
|
modifier = Modifier.size(size).then(modifier),
|
||||||
shape = RoundedCornerShape(radius),
|
shape = RoundedCornerShape(radius),
|
||||||
@@ -69,28 +81,51 @@ fun MusicCoverImage(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param decodeToDisplaySize 仅网格/列表缩略开启。详情大图勿开。
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalResourceApi::class)
|
@OptIn(ExperimentalResourceApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AlbumCoverImage(
|
fun AlbumCoverImage(
|
||||||
album: Album,
|
album: Album,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
radius: Dp = 12.dp,
|
radius: Dp = 12.dp,
|
||||||
|
decodeToDisplaySize: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val uri = remember(album.albumId, album.coverPath, album.existFile) {
|
val uri = remember(album.albumId, album.coverPath, album.existFile) {
|
||||||
AppContainer.pathResolver.coverDisplayUri(album)
|
AppContainer.pathResolver.coverDisplayUri(album)
|
||||||
}
|
}
|
||||||
CoverAsyncImage(
|
if (!decodeToDisplaySize) {
|
||||||
modelUri = uri,
|
CoverAsyncImage(
|
||||||
stableCacheKey = album.albumId?.let { albumCoverCacheKey(it) },
|
modelUri = uri,
|
||||||
modifier = modifier,
|
stableCacheKey = album.albumId?.let { albumCoverCacheKey(it) },
|
||||||
shape = RoundedCornerShape(radius),
|
decodeSize = null,
|
||||||
fallbackPadding = 6.dp,
|
modifier = modifier,
|
||||||
)
|
shape = RoundedCornerShape(radius),
|
||||||
|
fallbackPadding = 6.dp,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
BoxWithConstraints(modifier) {
|
||||||
|
val side = remember(maxWidth, maxHeight) {
|
||||||
|
measuredSquareSide(maxWidth, maxHeight)
|
||||||
|
}
|
||||||
|
CoverAsyncImage(
|
||||||
|
modelUri = uri,
|
||||||
|
stableCacheKey = album.albumId?.let { albumCoverCacheKey(it) },
|
||||||
|
decodeSize = side,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
shape = RoundedCornerShape(radius),
|
||||||
|
fallbackPadding = 6.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 艺人头像:48×48 圆形封面。
|
* 艺人头像:48×48 圆形封面。
|
||||||
* 加载 https `{R2}/LLMP-M/artist_webp/{uid}.webp`(经 Coil 网络请求)。
|
* 加载 https `{R2}/LLMP-M/artist_webp/{uid}.webp`(经 Coil 网络请求)。
|
||||||
|
*
|
||||||
|
* @param decodeToDisplaySize 仅列表缩略开启。
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalResourceApi::class)
|
@OptIn(ExperimentalResourceApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -98,6 +133,7 @@ fun ArtistPhotoImage(
|
|||||||
artist: Artist,
|
artist: Artist,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
size: Dp = 48.dp,
|
size: Dp = 48.dp,
|
||||||
|
decodeToDisplaySize: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val uri = remember(artist.uid, artist.photo) {
|
val uri = remember(artist.uid, artist.photo) {
|
||||||
artistPhotoDisplayUri(artist)
|
artistPhotoDisplayUri(artist)
|
||||||
@@ -105,6 +141,7 @@ fun ArtistPhotoImage(
|
|||||||
CoverAsyncImage(
|
CoverAsyncImage(
|
||||||
modelUri = uri,
|
modelUri = uri,
|
||||||
stableCacheKey = artistPhotoCacheKey(artist.uid),
|
stableCacheKey = artistPhotoCacheKey(artist.uid),
|
||||||
|
decodeSize = size.takeIf { decodeToDisplaySize },
|
||||||
modifier = Modifier.size(size).then(modifier),
|
modifier = Modifier.size(size).then(modifier),
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
fallbackPadding = 6.dp,
|
fallbackPadding = 6.dp,
|
||||||
@@ -120,8 +157,9 @@ private fun artistPhotoDisplayUri(artist: Artist): String? {
|
|||||||
return "${Const.R2_OSS_URL}LLMP-M/artist_webp/${artist.uid}.webp"
|
return "${Const.R2_OSS_URL}LLMP-M/artist_webp/${artist.uid}.webp"
|
||||||
}
|
}
|
||||||
|
|
||||||
/** WeSlide 迷你栏/全屏播放器/驾驶模式共用的封面组件。
|
/**
|
||||||
* `circular` 为 true 时圆形封面,否则圆角矩形。
|
* WeSlide 迷你栏/全屏播放器/驾驶模式共用的封面组件。
|
||||||
|
* 不做按显示尺寸解码(播放页需清晰大图)。
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalResourceApi::class)
|
@OptIn(ExperimentalResourceApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -136,6 +174,7 @@ fun PlayerMusicCover(
|
|||||||
CoverAsyncImage(
|
CoverAsyncImage(
|
||||||
modelUri = uri,
|
modelUri = uri,
|
||||||
stableCacheKey = music?.musicId?.let { musicCoverCacheKey(it) },
|
stableCacheKey = music?.musicId?.let { musicCoverCacheKey(it) },
|
||||||
|
decodeSize = null,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = if (circular) CircleShape else RoundedCornerShape(radius),
|
shape = if (circular) CircleShape else RoundedCornerShape(radius),
|
||||||
fallbackPadding = fallbackPadding,
|
fallbackPadding = fallbackPadding,
|
||||||
@@ -148,22 +187,34 @@ internal fun albumCoverCacheKey(albumId: String): String = "album-cover:$albumId
|
|||||||
|
|
||||||
internal fun artistPhotoCacheKey(uid: String): String = "artist-photo:$uid"
|
internal fun artistPhotoCacheKey(uid: String): String = "artist-photo:$uid"
|
||||||
|
|
||||||
|
private fun measuredSquareSide(maxWidth: Dp, maxHeight: Dp): Dp? =
|
||||||
|
when {
|
||||||
|
maxWidth.value.isFinite() && maxHeight.value.isFinite() ->
|
||||||
|
min(maxWidth, maxHeight).takeIf { it > 0.dp }
|
||||||
|
maxWidth.value.isFinite() -> maxWidth.takeIf { it > 0.dp }
|
||||||
|
maxHeight.value.isFinite() -> maxHeight.takeIf { it > 0.dp }
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalResourceApi::class)
|
@OptIn(ExperimentalResourceApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun CoverAsyncImage(
|
private fun CoverAsyncImage(
|
||||||
modelUri: String?,
|
modelUri: String?,
|
||||||
stableCacheKey: String? = null,
|
stableCacheKey: String? = null,
|
||||||
|
decodeSize: Dp? = null,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
shape: Shape,
|
shape: Shape,
|
||||||
fallbackPadding: Dp,
|
fallbackPadding: Dp,
|
||||||
) {
|
) {
|
||||||
val fallback = Res.getUri(FALLBACK_LOGO)
|
val fallback = Res.getUri(FALLBACK_LOGO)
|
||||||
|
val density = LocalDensity.current
|
||||||
val clipped = modifier.clip(shape).fillMaxSize()
|
val clipped = modifier.clip(shape).fillMaxSize()
|
||||||
if (modelUri.isNullOrBlank()) {
|
if (modelUri.isNullOrBlank()) {
|
||||||
LlmpAsyncImage(
|
LlmpAsyncImage(
|
||||||
model = fallback,
|
model = fallback,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
|
decodeSize = decodeSize,
|
||||||
modifier = clipped.padding(fallbackPadding),
|
modifier = clipped.padding(fallbackPadding),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -171,7 +222,7 @@ private fun CoverAsyncImage(
|
|||||||
|
|
||||||
// 进程级 Image 底图:LazyList dispose 会丢弃 remember(Painter),导致
|
// 进程级 Image 底图:LazyList dispose 会丢弃 remember(Painter),导致
|
||||||
// Loading(null) → 空白,即使磁盘/内存仍有封面。
|
// Loading(null) → 空白,即使磁盘/内存仍有封面。
|
||||||
val cacheKey = remember(modelUri, stableCacheKey) {
|
val baseCacheKey = remember(modelUri, stableCacheKey) {
|
||||||
stableCacheKey?.takeIf { it.isNotBlank() }
|
stableCacheKey?.takeIf { it.isNotBlank() }
|
||||||
?: if (modelUri.startsWith("http", ignoreCase = true)) {
|
?: if (modelUri.startsWith("http", ignoreCase = true)) {
|
||||||
modelUri.toImageCacheKey()
|
modelUri.toImageCacheKey()
|
||||||
@@ -179,6 +230,14 @@ private fun CoverAsyncImage(
|
|||||||
modelUri
|
modelUri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val bucketPx = remember(decodeSize, density) {
|
||||||
|
decodeSize?.let { dp ->
|
||||||
|
decodeSizeBucketPx(with(density) { dp.roundToPx() })
|
||||||
|
} ?: 0
|
||||||
|
}
|
||||||
|
val cacheKey = remember(baseCacheKey, bucketPx) {
|
||||||
|
sizedMemoryCacheKey(baseCacheKey, bucketPx)
|
||||||
|
}
|
||||||
val heldPainter = rememberCoverPainter(cacheKey)
|
val heldPainter = rememberCoverPainter(cacheKey)
|
||||||
var showLogoFallback by remember(cacheKey) { mutableStateOf(false) }
|
var showLogoFallback by remember(cacheKey) { mutableStateOf(false) }
|
||||||
var hasSuccess by remember(cacheKey) {
|
var hasSuccess by remember(cacheKey) {
|
||||||
@@ -198,6 +257,7 @@ private fun CoverAsyncImage(
|
|||||||
model = fallback,
|
model = fallback,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
|
decodeSize = decodeSize,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(fallbackPadding),
|
.padding(fallbackPadding),
|
||||||
@@ -208,7 +268,8 @@ private fun CoverAsyncImage(
|
|||||||
key(cacheKey) {
|
key(cacheKey) {
|
||||||
LlmpAsyncImage(
|
LlmpAsyncImage(
|
||||||
model = modelUri,
|
model = modelUri,
|
||||||
stableCacheKey = cacheKey,
|
stableCacheKey = baseCacheKey,
|
||||||
|
decodeSize = decodeSize,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|||||||
+6
-1
@@ -92,7 +92,12 @@ fun SongListItem(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
MusicCoverImage(music = music, size = ListLeadingCoverSize, radius = 12.dp)
|
MusicCoverImage(
|
||||||
|
music = music,
|
||||||
|
size = ListLeadingCoverSize,
|
||||||
|
radius = 12.dp,
|
||||||
|
decodeToDisplaySize = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(10.dp))
|
Spacer(Modifier.width(10.dp))
|
||||||
TwoLineListTextColumn(
|
TwoLineListTextColumn(
|
||||||
|
|||||||
+57
-11
@@ -10,6 +10,8 @@ import androidx.compose.ui.graphics.FilterQuality
|
|||||||
import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality
|
import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import coil3.Image
|
import coil3.Image
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.PlatformContext
|
import coil3.PlatformContext
|
||||||
@@ -31,6 +33,7 @@ import coil3.request.CachePolicy
|
|||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.Options
|
import coil3.request.Options
|
||||||
import coil3.request.crossfade
|
import coil3.request.crossfade
|
||||||
|
import coil3.size.Size
|
||||||
import coil3.svg.SvgDecoder
|
import coil3.svg.SvgDecoder
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
@@ -137,8 +140,9 @@ internal object ImageRevalidator {
|
|||||||
.networkCachePolicy(CachePolicy.ENABLED)
|
.networkCachePolicy(CachePolicy.ENABLED)
|
||||||
.crossfade(false)
|
.crossfade(false)
|
||||||
.listener(
|
.listener(
|
||||||
onSuccess = { _, result ->
|
onSuccess = { _, _ ->
|
||||||
CoverImageHolder.put(cacheKey, result.image)
|
// 只刷新 Coil 磁盘/内存;不写入 CoverImageHolder,
|
||||||
|
// 避免无尺寸的原图覆盖列表缩略图。
|
||||||
complete(cacheKey)
|
complete(cacheKey)
|
||||||
},
|
},
|
||||||
onError = { _, result ->
|
onError = { _, result ->
|
||||||
@@ -208,7 +212,8 @@ fun createLlmpImageLoader(context: PlatformContext): ImageLoader {
|
|||||||
.crossfade(false)
|
.crossfade(false)
|
||||||
.memoryCache {
|
.memoryCache {
|
||||||
MemoryCache.Builder()
|
MemoryCache.Builder()
|
||||||
.maxSizePercent(context, 0.35)
|
// 老机内存紧:略降占比,配合按显示尺寸解码。
|
||||||
|
.maxSizePercent(context, 0.20)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
.diskCache {
|
.diskCache {
|
||||||
@@ -230,6 +235,23 @@ fun createLlmpImageLoader(context: PlatformContext): ImageLoader {
|
|||||||
return loader
|
return loader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将显示边长收成缓存桶,减少 47/48/49dp 之类碎片键,
|
||||||
|
* 同时保证列表小图与播放页大图不会互相覆盖。
|
||||||
|
*/
|
||||||
|
internal fun decodeSizeBucketPx(px: Int): Int = when {
|
||||||
|
px <= 0 -> 0
|
||||||
|
px <= 64 -> 64
|
||||||
|
px <= 96 -> 96
|
||||||
|
px <= 144 -> 144
|
||||||
|
px <= 256 -> 256
|
||||||
|
px <= 384 -> 384
|
||||||
|
else -> 512
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun sizedMemoryCacheKey(baseKey: String, bucketPx: Int): String =
|
||||||
|
if (bucketPx > 0) "$baseKey@$bucketPx" else baseKey
|
||||||
|
|
||||||
/** 共享 AsyncImage:1 天网络缓存 + 过期先用旧图(无闪烁)。 */
|
/** 共享 AsyncImage:1 天网络缓存 + 过期先用旧图(无闪烁)。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun LlmpAsyncImage(
|
fun LlmpAsyncImage(
|
||||||
@@ -237,6 +259,8 @@ fun LlmpAsyncImage(
|
|||||||
contentDescription: String?,
|
contentDescription: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
stableCacheKey: String? = null,
|
stableCacheKey: String? = null,
|
||||||
|
/** 正方形解码目标(逻辑 dp)。传入后按像素桶解码,显著降低老机 CPU/内存。 */
|
||||||
|
decodeSize: Dp? = null,
|
||||||
transform: (State) -> State = DefaultTransform,
|
transform: (State) -> State = DefaultTransform,
|
||||||
onState: ((State) -> Unit)? = null,
|
onState: ((State) -> Unit)? = null,
|
||||||
alignment: Alignment = Alignment.Center,
|
alignment: Alignment = Alignment.Center,
|
||||||
@@ -247,24 +271,44 @@ fun LlmpAsyncImage(
|
|||||||
clipToBounds: Boolean = true,
|
clipToBounds: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val context = LocalPlatformContext.current
|
val context = LocalPlatformContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
val httpUrl = (model as? String)?.takeIf { it.isHttpUrl() }
|
val httpUrl = (model as? String)?.takeIf { it.isHttpUrl() }
|
||||||
val cacheKey = stableCacheKey?.takeIf { it.isNotBlank() }
|
val baseCacheKey = stableCacheKey?.takeIf { it.isNotBlank() }
|
||||||
?: httpUrl?.toImageCacheKey()
|
?: httpUrl?.toImageCacheKey()
|
||||||
|
val bucketPx = remember(decodeSize, density) {
|
||||||
|
decodeSize?.let { dp ->
|
||||||
|
decodeSizeBucketPx(with(density) { dp.roundToPx() })
|
||||||
|
} ?: 0
|
||||||
|
}
|
||||||
|
val memoryKey = baseCacheKey?.let { sizedMemoryCacheKey(it, bucketPx) }
|
||||||
|
val resolvedFilterQuality = when {
|
||||||
|
filterQuality != DefaultFilterQuality -> filterQuality
|
||||||
|
bucketPx in 1..96 -> FilterQuality.Low
|
||||||
|
else -> filterQuality
|
||||||
|
}
|
||||||
|
|
||||||
// 后台重新校验时不重挂载。TTL 刷新后重挂载可见封面
|
// 后台重新校验时不重挂载。TTL 刷新后重挂载可见封面
|
||||||
// 可能 Error 一次并使占位图锁定,行仍在屏幕上。
|
// 可能 Error 一次并使占位图锁定,行仍在屏幕上。
|
||||||
val request = remember(model, httpUrl, cacheKey) {
|
val request = remember(model, httpUrl, baseCacheKey, memoryKey, bucketPx) {
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(model)
|
.data(model)
|
||||||
.crossfade(false)
|
.crossfade(false)
|
||||||
.apply {
|
.apply {
|
||||||
if (cacheKey != null) {
|
if (bucketPx > 0) {
|
||||||
memoryCacheKey(cacheKey)
|
size(Size(bucketPx, bucketPx))
|
||||||
diskCacheKey(cacheKey)
|
}
|
||||||
placeholderMemoryCacheKey(cacheKey)
|
if (baseCacheKey != null) {
|
||||||
|
// 磁盘存原图字节(无尺寸后缀);内存按显示尺寸分桶。
|
||||||
|
diskCacheKey(baseCacheKey)
|
||||||
|
if (memoryKey != null) {
|
||||||
|
memoryCacheKey(memoryKey)
|
||||||
|
placeholderMemoryCacheKey(memoryKey)
|
||||||
|
}
|
||||||
listener(
|
listener(
|
||||||
onSuccess = { _, result ->
|
onSuccess = { _, result ->
|
||||||
CoverImageHolder.put(cacheKey, result.image)
|
if (memoryKey != null) {
|
||||||
|
CoverImageHolder.put(memoryKey, result.image)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError = { _, result ->
|
onError = { _, result ->
|
||||||
AppLog.w(
|
AppLog.w(
|
||||||
@@ -287,7 +331,7 @@ fun LlmpAsyncImage(
|
|||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
alpha = alpha,
|
alpha = alpha,
|
||||||
colorFilter = colorFilter,
|
colorFilter = colorFilter,
|
||||||
filterQuality = filterQuality,
|
filterQuality = resolvedFilterQuality,
|
||||||
clipToBounds = clipToBounds,
|
clipToBounds = clipToBounds,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -299,6 +343,7 @@ fun LlmpAsyncImage(
|
|||||||
contentDescription: String?,
|
contentDescription: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
stableCacheKey: String? = null,
|
stableCacheKey: String? = null,
|
||||||
|
decodeSize: Dp? = null,
|
||||||
onLoading: ((State.Loading) -> Unit)? = null,
|
onLoading: ((State.Loading) -> Unit)? = null,
|
||||||
onSuccess: ((State.Success) -> Unit)? = null,
|
onSuccess: ((State.Success) -> Unit)? = null,
|
||||||
onError: ((State.Error) -> Unit)? = null,
|
onError: ((State.Error) -> Unit)? = null,
|
||||||
@@ -314,6 +359,7 @@ fun LlmpAsyncImage(
|
|||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
stableCacheKey = stableCacheKey,
|
stableCacheKey = stableCacheKey,
|
||||||
|
decodeSize = decodeSize,
|
||||||
onState = { state ->
|
onState = { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
is State.Loading -> onLoading?.invoke(state)
|
is State.Loading -> onLoading?.invoke(state)
|
||||||
|
|||||||
+1
@@ -333,6 +333,7 @@ private fun MenuList(
|
|||||||
music = coverMusic,
|
music = coverMusic,
|
||||||
size = ListLeadingCoverSize,
|
size = ListLeadingCoverSize,
|
||||||
radius = 12.dp,
|
radius = 12.dp,
|
||||||
|
decodeToDisplaySize = true,
|
||||||
modifier = heroMod,
|
modifier = heroMod,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user