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") kapt("com.google.dagger:hilt-compiler:2.48")
// Image // Image
implementation("com.github.skydoves:landscapist-glide:2.1.11") implementation("io.coil-kt:coil-compose:2.4.0")
ksp("com.github.bumptech.glide:ksp:4.14.2") // this have to be align with landscapist-glide
} }
java { java {

View file

@ -1,7 +1,31 @@
package com.pixelized.rplexicon package com.pixelized.rplexicon
import android.app.Application import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.disk.DiskCache
import coil.memory.MemoryCache
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import java.io.File
@HiltAndroidApp @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 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.Composable
import androidx.compose.runtime.Stable import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.DefaultAlpha
import com.bumptech.glide.request.RequestListener import androidx.compose.ui.graphics.FilterQuality
import com.pixelized.rplexicon.utilitary.rememberLoadingTransition import androidx.compose.ui.graphics.drawscope.DrawScope
import com.skydoves.landscapist.ImageOptions import androidx.compose.ui.layout.ContentScale
import com.skydoves.landscapist.InternalLandscapistApi import coil.compose.AsyncImagePainter
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
@Composable @Composable
fun AsyncImage( fun AsyncImage(
imageModel: () -> Any?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
glideRequestType: GlideRequestType = GlideRequestType.DRAWABLE, model: Any?,
requestListener: (() -> RequestListener<Any>)? = null, contentDescription: String? = null,
component: ImageComponent = rememberImageComponent {}, transform: (AsyncImagePainter.State) -> AsyncImagePainter.State = AsyncImagePainter.DefaultTransform,
imageOptions: ImageOptions = ImageOptions(), onState: ((AsyncImagePainter.State) -> Unit)? = null,
onImageStateChanged: (GlideImageState) -> Unit = {}, alignment: Alignment = Alignment.Center,
@DrawableRes previewPlaceholder: Int = 0, contentScale: ContentScale = ContentScale.Fit,
loading: @Composable (BoxScope.(imageState: GlideImageState.Loading) -> Unit)? = null, alpha: Float = DefaultAlpha,
success: @Composable (BoxScope.(imageState: GlideImageState.Success, painter: Painter) -> Unit)? = null, colorFilter: ColorFilter? = null,
failure: @Composable (BoxScope.(imageState: GlideImageState.Failure) -> Unit)? = null, filterQuality: FilterQuality = DrawScope.DefaultFilterQuality,
) { ) {
val transition = rememberLoadingTransition(imageModel) coil.compose.AsyncImage(
modifier = modifier,
GlideImage( model = model,
imageModel = imageModel, contentDescription = contentDescription,
modifier = modifier.alpha(alpha = transition.alpha), alignment = alignment,
glideRequestType = glideRequestType, contentScale = contentScale,
requestListener = requestListener, alpha = alpha,
component = component, colorFilter = colorFilter,
imageOptions = imageOptions, transform = transform,
onImageStateChanged = { onState = onState,
when (it) { filterQuality = filterQuality,
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,
) )
} }

View file

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

View file

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

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

View file

@ -203,7 +203,7 @@ private fun QuestDetailContent(
) { ) {
BackgroundImage( BackgroundImage(
modifier = Modifier.matchParentSize(), modifier = Modifier.matchParentSize(),
model = { annotatedQuest.background }, model = annotatedQuest.background,
) )
if (annotatedQuest.completed) { if (annotatedQuest.completed) {
Text( 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.FiniteAnimationSpec
import androidx.compose.animation.core.Transition 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.animation.core.spring
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable 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.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -19,12 +23,20 @@ import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf 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.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape 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.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
@ -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 @Stable
fun Modifier.cell() = composed { fun Modifier.cell() = composed {
Modifier Modifier