更新图片缓存

This commit is contained in:
2024-08-22 23:43:01 +08:00
parent a4c8dcb9aa
commit 5e65b7fe4d
8 changed files with 342 additions and 21 deletions

View File

@@ -55,7 +55,7 @@ data class AccountProfile(
followerCount = followerCount,
followingCount = followingCount,
nickName = nickname,
avatar = ApiClient.BASE_SERVER + avatar + "?token=${AppStore.token}",
avatar = "${ApiClient.BASE_SERVER}$avatar",
bio = "",
country = "Worldwide",
isFollowing = isFollowing
@@ -86,8 +86,8 @@ data class NoticePost(
textContent = textContent,
images = images.map {
it.copy(
url = ApiClient.BASE_SERVER + it.url + "?token=${AppStore.token}",
thumbnail = ApiClient.BASE_SERVER + it.thumbnail + "?token=${AppStore.token}",
url = "${ApiClient.BASE_SERVER}${it.url}",
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}",
)
},
time = ApiClient.dateFromApiString(time)
@@ -113,7 +113,7 @@ data class NoticeUser(
return NoticeUserEntity(
id = id,
nickName = nickName,
avatar = ApiClient.BASE_SERVER + avatar + "?token=${AppStore.token}",
avatar = "${ApiClient.BASE_SERVER}$avatar",
)
}
}
@@ -259,7 +259,7 @@ class FollowItemPagingSource(
LoadResult.Page(
data = followListContainer.list.map {
it.copy(
avatar = ApiClient.BASE_SERVER + it.avatar + "?token=${AppStore.token}",
avatar = "${ApiClient.BASE_SERVER}${it.avatar}",
)
},
prevKey = if (currentPage == 1) null else currentPage - 1,

View File

@@ -55,7 +55,7 @@ data class Comment(
likes = likeCount,
replies = emptyList(),
postId = postId,
avatar = ApiClient.BASE_SERVER + user.avatar + "?token=${AppStore.token}",
avatar = "${ApiClient.BASE_SERVER}${user.avatar}",
author = user.id,
liked = isLiked,
unread = isUnread,
@@ -63,8 +63,8 @@ data class Comment(
it.copy(
images = it.images.map {
it.copy(
url = ApiClient.BASE_SERVER + it.url + "?token=${AppStore.token}",
thumbnail = ApiClient.BASE_SERVER + it.thumbnail + "?token=${AppStore.token}"
url = "${ApiClient.BASE_SERVER}${it.url}",
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}"
)
}
)

View File

@@ -44,10 +44,10 @@ data class Moment(
val time: String
) {
fun toMomentItem(): MomentEntity {
val avatar = ApiClient.BASE_SERVER + user.avatar + "?token=${AppStore.token}"
val avatar = "${ApiClient.BASE_SERVER}${user.avatar}"
return MomentEntity(
id = id.toInt(),
avatar = ApiClient.BASE_SERVER + user.avatar + "?token=${AppStore.token}",
avatar = "${ApiClient.BASE_SERVER}${user.avatar}",
nickname = user.nickName,
location = "Worldwide",
time = time,
@@ -60,9 +60,10 @@ data class Moment(
favoriteCount = favoriteCount.toInt(),
images = images.map {
MomentImageEntity(
url = ApiClient.BASE_SERVER + it.url + "?token=${AppStore.token}",
thumbnail = ApiClient.BASE_SERVER + it.thumbnail + "?token=${AppStore.token}",
id = it.id
url = "${ApiClient.BASE_SERVER}${it.url}",
thumbnail = "${ApiClient.BASE_SERVER}${it.thumbnail}",
id = it.id,
blurHash = it.blurHash
)
},
authorId = user.id.toInt(),

View File

@@ -1,11 +1,14 @@
package com.aiosman.riderpro.model
import androidx.annotation.DrawableRes
data class MomentImageEntity(
val id: Long,
val url: String,
val thumbnail: String
val thumbnail: String,
val blurHash: String? = null
)
data class MomentEntity(
val id: Int,
val avatar: String,

View File

@@ -0,0 +1,81 @@
package com.aiosman.riderpro.ui.composables
import android.content.Context
import android.graphics.drawable.BitmapDrawable
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.aiosman.riderpro.utils.BlurHashDecoder
import com.aiosman.riderpro.utils.Utils.getImageLoader
private const val DEFAULT_HASHED_BITMAP_WIDTH = 4
private const val DEFAULT_HASHED_BITMAP_HEIGHT = 3
/**
* This function is used to load an image asynchronously and blur it using BlurHash.
* @param imageUrl The URL of the image to be loaded.
* @param modifier The modifier to be applied to the image.
* @param imageModifier The modifier to be applied to the image.
* @param contentDescription The content description to be applied to the image.
* @param contentScale The content scale to be applied to the image.
* @param isCrossFadeRequired Whether cross-fade is required or not.
* @param onImageLoadSuccess The callback to be called when the image is loaded successfully.
* @param onImageLoadFailure The callback to be called when the image is failed to load.
* @see AsyncImage
*/
@Suppress("LongParameterList")
@ExperimentalCoilApi
@Composable
fun AsyncBlurImage(
imageUrl: String,
blurHash: String,
modifier: Modifier = Modifier,
imageModifier: Modifier? = null,
contentDescription: String? = null,
contentScale: ContentScale = ContentScale.Fit,
isCrossFadeRequired: Boolean = false,
onImageLoadSuccess: () -> Unit = {},
onImageLoadFailure: () -> Unit = {}
) {
val context = LocalContext.current
val resources = context.resources
val imageLoader = getImageLoader(context)
val blurBitmap by remember(blurHash) {
mutableStateOf(
BlurHashDecoder.decode(
blurHash = blurHash,
width = DEFAULT_HASHED_BITMAP_WIDTH,
height = DEFAULT_HASHED_BITMAP_HEIGHT
)
)
}
AsyncImage(
modifier = imageModifier ?: modifier,
model = ImageRequest.Builder(context)
.data(imageUrl)
.crossfade(isCrossFadeRequired)
.placeholder(
blurBitmap?.toDrawable(resources)
)
.fallback(blurBitmap?.toDrawable(resources))
.build(),
contentDescription = contentDescription,
contentScale = contentScale,
onSuccess = { onImageLoadSuccess() },
onError = { onImageLoadFailure() },
imageLoader = imageLoader
)
}

View File

@@ -59,7 +59,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.AsyncImage
import com.aiosman.riderpro.LocalAnimatedContentScope
import com.aiosman.riderpro.LocalNavController
import com.aiosman.riderpro.LocalSharedTransitionScope
@@ -71,9 +70,9 @@ import com.aiosman.riderpro.ui.comment.CommentModalContent
import com.aiosman.riderpro.ui.composables.AnimatedCounter
import com.aiosman.riderpro.ui.composables.AnimatedFavouriteIcon
import com.aiosman.riderpro.ui.composables.AnimatedLikeIcon
import com.aiosman.riderpro.ui.composables.AsyncBlurImage
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
import com.aiosman.riderpro.ui.composables.RelPostCard
import com.aiosman.riderpro.ui.imageviewer.ImageViewerViewModel
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
import com.aiosman.riderpro.ui.post.NewPostViewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -353,6 +352,18 @@ fun PostImageView(
) { page ->
val image = images[page]
with(sharedTransitionScope) {
// AsyncBlurImage(
// imageUrl = image.url,
// blurHash = image.blurHash ?: "",
// contentDescription = "Image",
// contentScale = ContentScale.Crop,
// modifier = Modifier
// .sharedElement(
// rememberSharedContentState(key = image),
// animatedVisibilityScope = animatedVisibilityScope
// )
// .fillMaxSize()
// )
CustomAsyncImage(
context,
image.thumbnail,

View File

@@ -0,0 +1,218 @@
package com.aiosman.riderpro.utils
import android.graphics.Bitmap
import android.graphics.Color
import androidx.collection.SparseArrayCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.withSign
internal object BlurHashDecoder {
// cache Math.cos() calculations to improve performance.
// The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps
// the cache is enabled by default, it is recommended to disable it only when just a few images are displayed
private val cacheCosinesX = SparseArrayCompat<DoubleArray>()
private val cacheCosinesY = SparseArrayCompat<DoubleArray>()
/**
* Clear calculations stored in memory cache.
* The cache is not big, but will increase when many image sizes are used,
* if the app needs memory it is recommended to clear it.
*/
private fun clearCache() {
cacheCosinesX.clear()
cacheCosinesY.clear()
}
/**
* Decode a blur hash into a new bitmap.
*
* @param useCache use in memory cache for the calculated math, reused by images with same size.
* if the cache does not exist yet it will be created and populated with new calculations.
* By default it is true.
*/
@Suppress("ReturnCount")
internal fun decode(
blurHash: String?,
width: Int,
height: Int,
punch: Float = 1f,
useCache: Boolean = true
): Bitmap? {
if (blurHash == null || blurHash.length < 6) {
return null
}
val numCompEnc = decode83(blurHash, 0, 1)
val numCompX = (numCompEnc % 9) + 1
val numCompY = (numCompEnc / 9) + 1
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
return null
}
val maxAcEnc = decode83(blurHash, 1, 2)
val maxAc = (maxAcEnc + 1) / 166f
val colors = Array(numCompX * numCompY) { i ->
if (i == 0) {
val colorEnc = decode83(blurHash, 2, 6)
decodeDc(colorEnc)
} else {
val from = 4 + i * 2
val colorEnc = decode83(blurHash, from, from + 2)
decodeAc(colorEnc, maxAc * punch)
}
}
return composeBitmap(width, height, numCompX, numCompY, colors, useCache)
}
private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
var result = 0
for (i in from until to) {
val index = charMap[str[i]] ?: -1
if (index != -1) {
result = result * 83 + index
}
}
return result
}
private fun decodeDc(colorEnc: Int): FloatArray {
val r = colorEnc shr 16
val g = (colorEnc shr 8) and 255
val b = colorEnc and 255
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
}
private fun srgbToLinear(colorEnc: Int): Float {
val v = colorEnc / 255f
return if (v <= 0.04045f) {
(v / 12.92f)
} else {
((v + 0.055f) / 1.055f).pow(2.4f)
}
}
private fun decodeAc(value: Int, maxAc: Float): FloatArray {
val r = value / (19 * 19)
val g = (value / 19) % 19
val b = value % 19
return floatArrayOf(
signedPow2((r - 9) / 9.0f) * maxAc,
signedPow2((g - 9) / 9.0f) * maxAc,
signedPow2((b - 9) / 9.0f) * maxAc
)
}
private fun signedPow2(value: Float) = value.pow(2f).withSign(value)
private fun composeBitmap(
width: Int,
height: Int,
numCompX: Int,
numCompY: Int,
colors: Array<FloatArray>,
useCache: Boolean
): Bitmap {
// use an array for better performance when writing pixel colors
val imageArray = IntArray(width * height)
val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX)
val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX)
val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY)
val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY)
runBlocking {
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
val tasks = ArrayList<Deferred<Unit>>()
tasks.add(
async {
for (y in 0 until height) {
for (x in 0 until width) {
var r = 0f
var g = 0f
var b = 0f
for (j in 0 until numCompY) {
for (i in 0 until numCompX) {
val cosX =
cosinesX.getCos(calculateCosX, i, numCompX, x, width)
val cosY =
cosinesY.getCos(calculateCosY, j, numCompY, y, height)
val basis = (cosX * cosY).toFloat()
val color = colors[j * numCompX + i]
r += color[0] * basis
g += color[1] * basis
b += color[2] * basis
}
}
imageArray[x + width * y] =
Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))
}
}
return@async
}
)
tasks.forEach { it.await() }
}.join()
}
return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888)
}
private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when {
calculate -> {
DoubleArray(height * numCompY).also {
cacheCosinesY.put(height * numCompY, it)
}
}
else -> {
cacheCosinesY.get(height * numCompY)!!
}
}
private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when {
calculate -> {
DoubleArray(width * numCompX).also {
cacheCosinesX.put(width * numCompX, it)
}
}
else -> cacheCosinesX.get(width * numCompX)!!
}
private fun DoubleArray.getCos(
calculate: Boolean,
x: Int,
numComp: Int,
y: Int,
size: Int
): Double {
if (calculate) {
this[x + numComp * y] = cos(Math.PI * y * x / size)
}
return this[x + numComp * y]
}
private fun linearToSrgb(value: Float): Int {
val v = value.coerceIn(0f, 1f)
return if (v <= 0.0031308f) {
(v * 12.92f * 255f + 0.5f).toInt()
} else {
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
}
}
private val charMap = listOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
)
.mapIndexed { i, c -> c to i }
.toMap()
}

View File

@@ -2,6 +2,8 @@ package com.aiosman.riderpro.utils
import android.content.Context
import coil.ImageLoader
import coil.disk.DiskCache
import coil.memory.MemoryCache
import coil.request.CachePolicy
import com.aiosman.riderpro.data.api.getUnsafeOkHttpClient
import java.util.Date
@@ -19,11 +21,16 @@ object Utils {
val okHttpClient = getUnsafeOkHttpClient()
return ImageLoader.Builder(context)
.okHttpClient(okHttpClient)
.diskCachePolicy(CachePolicy.ENABLED)
.memoryCachePolicy(CachePolicy.ENABLED)
.components {
.memoryCache {
MemoryCache.Builder(context)
.maxSizePercent(0.25) // 设置内存缓存大小为可用内存的 25%
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache"))
.maxSizePercent(0.02) // 设置磁盘缓存大小为可用存储空间的 2%
.build()
}.build()
}