Add detail image dialog and change glide to coil.

This commit is contained in:
Thomas Andres Gomez 2023-10-13 13:00:26 +02:00
parent d6072e9a00
commit 4070a6e5fe
12 changed files with 235 additions and 151 deletions

View file

@ -142,8 +142,7 @@ dependencies {
kapt("com.google.dagger:hilt-compiler:2.48")
// Image
implementation("com.github.skydoves:landscapist-glide:2.1.11")
ksp("com.github.bumptech.glide:ksp:4.14.2") // this have to be align with landscapist-glide
implementation("io.coil-kt:coil-compose:2.4.0")
}
java {

View file

@ -1,7 +1,31 @@
package com.pixelized.rplexicon
import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.disk.DiskCache
import coil.memory.MemoryCache
import dagger.hilt.android.HiltAndroidApp
import java.io.File
@HiltAndroidApp
class MainApplication : Application()
class MainApplication : Application(), ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.memoryCache {
MemoryCache.Builder(this)
.maxSizePercent(0.25)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(applicationContext.cacheDir.resolve("image_cache"))
.maxSizeBytes(size = 150.Mo)
.build()
}
.build()
}
}
val Int.Mo: Long get() = (this * 1024 * 1024).toLong()

View file

@ -1,57 +1,38 @@
package com.pixelized.rplexicon.ui.composable
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.painter.Painter
import com.bumptech.glide.request.RequestListener
import com.pixelized.rplexicon.utilitary.rememberLoadingTransition
import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.InternalLandscapistApi
import com.skydoves.landscapist.components.ImageComponent
import com.skydoves.landscapist.components.rememberImageComponent
import com.skydoves.landscapist.glide.GlideImage
import com.skydoves.landscapist.glide.GlideImageState
import com.skydoves.landscapist.glide.GlideRequestType
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.ContentScale
import coil.compose.AsyncImagePainter
@Composable
fun AsyncImage(
imageModel: () -> Any?,
modifier: Modifier = Modifier,
glideRequestType: GlideRequestType = GlideRequestType.DRAWABLE,
requestListener: (() -> RequestListener<Any>)? = null,
component: ImageComponent = rememberImageComponent {},
imageOptions: ImageOptions = ImageOptions(),
onImageStateChanged: (GlideImageState) -> Unit = {},
@DrawableRes previewPlaceholder: Int = 0,
loading: @Composable (BoxScope.(imageState: GlideImageState.Loading) -> Unit)? = null,
success: @Composable (BoxScope.(imageState: GlideImageState.Success, painter: Painter) -> Unit)? = null,
failure: @Composable (BoxScope.(imageState: GlideImageState.Failure) -> Unit)? = null,
model: Any?,
contentDescription: String? = null,
transform: (AsyncImagePainter.State) -> AsyncImagePainter.State = AsyncImagePainter.DefaultTransform,
onState: ((AsyncImagePainter.State) -> Unit)? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
) {
val transition = rememberLoadingTransition(imageModel)
GlideImage(
imageModel = imageModel,
modifier = modifier.alpha(alpha = transition.alpha),
glideRequestType = glideRequestType,
requestListener = requestListener,
component = component,
imageOptions = imageOptions,
onImageStateChanged = {
when (it) {
is GlideImageState.Success -> transition.target = 1f
is GlideImageState.Failure -> transition.target = 1f
is GlideImageState.Loading -> transition.target = 0f
is GlideImageState.None -> transition.target = 0f
}
onImageStateChanged(it)
},
previewPlaceholder = previewPlaceholder,
loading = loading,
success = success,
failure = failure,
coil.compose.AsyncImage(
modifier = modifier,
model = model,
contentDescription = contentDescription,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
transform = transform,
onState = onState,
filterQuality = filterQuality,
)
}

View file

@ -16,7 +16,6 @@ import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import com.pixelized.rplexicon.R
import com.skydoves.landscapist.ImageOptions
@Composable
@ -52,20 +51,17 @@ fun BackgroundImage(
ColorMatrix().also { it.setToSaturation(0f) }
)
},
model: () -> Any?,
model: Any?,
) {
Box(
modifier = modifier
) {
AsyncImage(
modifier = Modifier.matchParentSize(),
imageOptions = ImageOptions(
alignment = alignment,
contentScale = contentScale,
colorFilter = colorFilter,
),
imageModel = model,
previewPlaceholder = R.drawable.im_brulkhai,
alignment = alignment,
contentScale = contentScale,
colorFilter = colorFilter,
model = model,
)
Box(
modifier = Modifier

View file

@ -0,0 +1,108 @@
package com.pixelized.rplexicon.ui.composable
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.uri
import com.pixelized.rplexicon.utilitary.extentions.zoomable
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class FullScreenImageViewModel @Inject constructor() : ViewModel() {
private val _url = mutableStateOf<Uri?>(null)
val url: State<Uri?> get() = _url
fun showDetail(url: Uri) {
_url.value = url
}
fun hideDetail() {
_url.value = null
}
}
@Composable
fun FullScreenImageHandler(
viewModel: FullScreenImageViewModel = hiltViewModel(),
) {
ImageDialog(
url = viewModel.url,
onDismissRequest = viewModel::hideDetail,
)
}
@Composable
fun ImageDialog(
url: State<Uri?>,
onDismissRequest: () -> Unit,
) {
val uri by url
if (uri != null) {
Dialog(
properties = remember {
DialogProperties(
decorFitsSystemWindows = false,
usePlatformDefaultWidth = false,
)
},
onDismissRequest = onDismissRequest,
) {
Box {
AsyncImage(
modifier = Modifier
.fillMaxSize()
.zoomable()
.padding(all = 16.dp),
contentScale = ContentScale.Fit,
model = uri,
)
IconButton(
modifier = Modifier.align(alignment = Alignment.TopEnd),
onClick = onDismissRequest,
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
)
}
}
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun ImageDialogPreview() {
LexiconTheme {
ImageDialog(
url = remember { mutableStateOf(Uri.parse(R.drawable.im_brulkhai.uri)) },
onDismissRequest = { },
)
}
}

View file

@ -10,7 +10,6 @@ import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.pixelized.rplexicon.ui.navigation.screens.AUTHENTICATION_ROUTE
import com.pixelized.rplexicon.ui.navigation.screens.HOME_ROUTE
import com.pixelized.rplexicon.ui.navigation.screens.composableAuthentication
import com.pixelized.rplexicon.ui.navigation.screens.composableCharacterSheet
import com.pixelized.rplexicon.ui.navigation.screens.composableHome

View file

@ -5,6 +5,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
@ -18,7 +19,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
@ -58,6 +58,8 @@ import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.ui.composable.AsyncImage
import com.pixelized.rplexicon.ui.composable.BackgroundImage
import com.pixelized.rplexicon.ui.composable.FullScreenImageHandler
import com.pixelized.rplexicon.ui.composable.FullScreenImageViewModel
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterSheet
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@ -68,7 +70,6 @@ import com.pixelized.rplexicon.utilitary.extentions.highlightRegex
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.scrollOffset
import com.pixelized.rplexicon.utilitary.extentions.searchCriterion
import com.skydoves.landscapist.ImageOptions
@Stable
data class LexiconDetailUio(
@ -160,6 +161,7 @@ fun LexiconDetailUio.annotate(): AnnotatedLexiconDetailUio {
@Composable
fun LexiconDetailScreen(
viewModel: LexiconDetailViewModel = hiltViewModel(),
imageViewModel: FullScreenImageViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current
@ -170,6 +172,11 @@ fun LexiconDetailScreen(
haveCharacterSheet = viewModel.haveCharacterSheet,
onBack = { screen.popBackStack() },
onCharacterSheet = { screen.navigateToCharacterSheet(name = it) },
onImage = { imageViewModel.showDetail(it) }
)
FullScreenImageHandler(
viewModel = imageViewModel,
)
}
}
@ -183,6 +190,7 @@ private fun LexiconDetailContent(
haveCharacterSheet: State<Boolean>,
onBack: () -> Unit,
onCharacterSheet: (String) -> Unit,
onImage: (Uri) -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography
@ -230,7 +238,7 @@ private fun LexiconDetailContent(
.fillMaxWidth()
.aspectRatio(ratio = 1f)
.scrollOffset(scrollState = state) { -it / 2 },
model = { uri.toString() },
model = uri,
)
}
Column(
@ -384,17 +392,16 @@ private fun LexiconDetailContent(
) {
items(items = annotatedItem.portrait) {
AsyncImage(
modifier = Modifier.sizeIn(
minWidth = maxSize / 2,
maxWidth = maxSize,
minHeight = maxSize,
maxHeight = maxSize,
),
imageOptions = ImageOptions(
contentScale = ContentScale.FillHeight
),
imageModel = { it },
previewPlaceholder = R.drawable.im_brulkhai,
modifier = Modifier
.clickable { onImage(it) }
.sizeIn(
minWidth = maxSize / 2,
maxWidth = maxSize,
minHeight = maxSize,
maxHeight = maxSize,
),
contentScale = ContentScale.FillHeight,
model = it,
)
}
}
@ -459,6 +466,7 @@ private fun LexiconDetailPreview() {
haveCharacterSheet = remember { mutableStateOf(true) },
onBack = { },
onCharacterSheet = { },
onImage = { },
)
}
}

View file

@ -1,6 +1,5 @@
package com.pixelized.rplexicon.ui.screens.location.detail
import androidx.annotation.DrawableRes
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.animateOffsetAsState
import androidx.compose.foundation.gestures.detectTapGestures
@ -26,21 +25,20 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.composable.AsyncImage
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.skydoves.landscapist.ImageOptions
@Composable
fun FantasyMap(
modifier: Modifier = Modifier,
state: FantasyMapState,
model: () -> Any?,
imageOptions: ImageOptions = ImageOptions(),
@DrawableRes previewPlaceholder: Int,
model: Any?,
contentScale: ContentScale = ContentScale.Fit,
items: State<List<AnnotatedMarqueeUio>>,
highlight: State<Offset>,
selectedItem: State<Int>,
@ -163,9 +161,8 @@ fun FantasyMap(
}
)
},
imageModel = model,
imageOptions = imageOptions,
previewPlaceholder = previewPlaceholder,
model = model,
contentScale = contentScale,
)
}
}

View file

@ -74,7 +74,6 @@ import com.pixelized.rplexicon.ui.composable.Handle
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.skydoves.landscapist.ImageOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@ -292,9 +291,8 @@ private fun LocationContent(
)
.offset(scrollState = scrollState),
state = fantasyMapState,
model = { item.value.map },
previewPlaceholder = R.drawable.im_brulkhai,
imageOptions = ImageOptions(contentScale = ContentScale.Fit),
model = item.value.map,
contentScale = ContentScale.Fit,
items = remember { derivedStateOf { item.value.marquees } },
selectedItem = selectedIndex,
highlight = mapHighlight,

View file

@ -203,7 +203,7 @@ private fun QuestDetailContent(
) {
BackgroundImage(
modifier = Modifier.matchParentSize(),
model = { annotatedQuest.background },
model = annotatedQuest.background,
)
if (annotatedQuest.completed) {
Text(

View file

@ -1,65 +0,0 @@
package com.pixelized.rplexicon.utilitary
import android.content.Context
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalView
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.module.AppGlideModule
@GlideModule
class LexiconGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
val diskCacheSizeBytes = 1024 * 1024 * 100 // 100 MB
builder.setDiskCache(InternalCacheDiskCacheFactory(context, diskCacheSizeBytes.toLong()))
}
}
@Stable
class GlideLoadingTransition(
target: MutableState<Float>,
alpha: State<Float>
) {
var target by target
val alpha by alpha
}
@Composable
fun rememberLoadingTransition(model: () -> Any?): GlideLoadingTransition {
val isInEditMode = LocalView.current.isInEditMode
return if (isInEditMode) {
remember {
GlideLoadingTransition(
target = mutableFloatStateOf(1f),
alpha = mutableFloatStateOf(1f),
)
}
} else {
val key = model()
val target = remember(key) {
mutableFloatStateOf(0f)
}
val alpha = animateFloatAsState(
targetValue = target.value,
label = "LoadingAlphaTransition"
)
remember(key) {
GlideLoadingTransition(
target = target,
alpha = alpha,
)
}
}
}

View file

@ -2,11 +2,15 @@ package com.pixelized.rplexicon.utilitary.extentions
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.animateOffsetAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxWidth
@ -19,12 +23,20 @@ import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -49,6 +61,33 @@ fun Modifier.placeholder(
)
}
fun Modifier.zoomable() = composed {
var offset by remember { mutableStateOf(Offset.Zero) }
var zoom by remember { mutableFloatStateOf(1f) }
return@composed this
.pointerInput(Unit) {
detectTransformGestures(
onGesture = { centroid, pan, gestureZoom, _ ->
val newScale = maxOf(1f, zoom * gestureZoom)
val newOffset = (offset + centroid / zoom) - (centroid / newScale + pan / zoom)
offset = Offset(
newOffset.x.coerceIn(0f, (size.width / zoom) * (zoom - 1f)),
newOffset.y.coerceIn(0f, (size.height / zoom) * (zoom - 1f))
)
zoom = maxOf(1f, zoom * gestureZoom)
}
)
}
.graphicsLayer {
translationX = -offset.x * zoom
translationY = -offset.y * zoom
scaleX = zoom
scaleY = zoom
transformOrigin = TransformOrigin(0f, 0f)
}
}
@Stable
fun Modifier.cell() = composed {
Modifier