更新图片缓存
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
218
app/src/main/java/com/aiosman/riderpro/utils/BlurHashDecoder.kt
Normal file
218
app/src/main/java/com/aiosman/riderpro/utils/BlurHashDecoder.kt
Normal 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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user