@@ -34,11 +34,17 @@ import androidx.compose.runtime.*
3434import androidx.compose.ui.Alignment
3535import androidx.compose.ui.Modifier
3636import androidx.compose.ui.draw.clip
37+ import androidx.compose.ui.geometry.Offset
3738import androidx.compose.ui.graphics.Color
3839import androidx.compose.ui.graphics.PathEffect
40+ import androidx.compose.ui.graphics.graphicsLayer
41+ import androidx.compose.ui.input.pointer.pointerInput
3942import androidx.compose.ui.layout.ContentScale
4043import androidx.compose.ui.platform.LocalContext
44+ import androidx.compose.foundation.gestures.detectTransformGestures
4145import androidx.compose.ui.res.stringResource
46+ import coil.request.ImageRequest
47+ import coil.size.Size
4248import androidx.compose.ui.text.font.FontFamily
4349import androidx.compose.ui.unit.dp
4450import androidx.compose.ui.unit.sp
@@ -83,6 +89,10 @@ fun UpscaleScreen(
8389 var backendLogs by remember { mutableStateOf<List <String >>(emptyList()) }
8490 var currentLog by remember { mutableStateOf(" " ) }
8591
92+ var sharedScale by remember { mutableFloatStateOf(1f ) }
93+ var sharedOffsetX by remember { mutableFloatStateOf(0f ) }
94+ var sharedOffsetY by remember { mutableFloatStateOf(0f ) }
95+
8696 var showUpscalerDialog by remember { mutableStateOf(false ) }
8797 val upscalerRepository = remember { UpscalerRepository (context) }
8898 val upscalerPreferences =
@@ -116,6 +126,11 @@ fun UpscaleScreen(
116126 } else {
117127 selectedImageUri = it
118128 selectedBitmap = bitmap
129+ withContext(Dispatchers .Main ) {
130+ sharedScale = 1f
131+ sharedOffsetX = 0f
132+ sharedOffsetY = 0f
133+ }
119134 }
120135 }
121136 } catch (e: Exception ) {
@@ -356,13 +371,21 @@ fun UpscaleScreen(
356371 )
357372 }
358373 } else {
359- AsyncImage (
360- model = selectedImageUri,
374+ ZoomableImage (
375+ imageUri = selectedImageUri,
361376 contentDescription = stringResource(R .string.selected_image),
362377 modifier = Modifier
363378 .fillMaxSize()
364379 .padding(8 .dp),
365- contentScale = ContentScale .Fit
380+ scale = sharedScale,
381+ offsetX = sharedOffsetX,
382+ offsetY = sharedOffsetY,
383+ onTransform = { newScale, newOffsetX, newOffsetY ->
384+ sharedScale = newScale
385+ sharedOffsetX = newOffsetX
386+ sharedOffsetY = newOffsetY
387+ },
388+ useOriginalSize = true
366389 )
367390 }
368391
@@ -371,6 +394,9 @@ fun UpscaleScreen(
371394 onClick = {
372395 selectedImageUri = null
373396 selectedBitmap = null
397+ sharedScale = 1f
398+ sharedOffsetX = 0f
399+ sharedOffsetY = 0f
374400 },
375401 modifier = Modifier
376402 .align(Alignment .TopEnd )
@@ -439,13 +465,21 @@ fun UpscaleScreen(
439465 shape = RoundedCornerShape (12 .dp)
440466 )
441467 ) {
442- AsyncImage (
443- model = upscaledImageUri,
468+ ZoomableImage (
469+ imageUri = upscaledImageUri,
444470 contentDescription = stringResource(R .string.upscaled_image),
445471 modifier = Modifier
446472 .fillMaxSize()
447473 .padding(8 .dp),
448- contentScale = ContentScale .Fit
474+ scale = sharedScale,
475+ offsetX = sharedOffsetX,
476+ offsetY = sharedOffsetY,
477+ onTransform = { newScale, newOffsetX, newOffsetY ->
478+ sharedScale = newScale
479+ sharedOffsetX = newOffsetX
480+ sharedOffsetY = newOffsetY
481+ },
482+ useOriginalSize = true
449483 )
450484
451485 FilledTonalIconButton (
@@ -694,3 +728,71 @@ fun prepareRuntimeDir(context: Context): File {
694728
695729 return runtimeDir
696730}
731+
732+ @Composable
733+ fun ZoomableImage (
734+ imageUri : Uri ? ,
735+ contentDescription : String? ,
736+ modifier : Modifier = Modifier ,
737+ scale : Float ,
738+ offsetX : Float ,
739+ offsetY : Float ,
740+ onTransform : (scale: Float , offsetX: Float , offsetY: Float ) -> Unit ,
741+ useOriginalSize : Boolean = false
742+ ) {
743+ val context = LocalContext .current
744+
745+ var currentScale by remember { mutableFloatStateOf(scale) }
746+ var currentOffsetX by remember { mutableFloatStateOf(offsetX) }
747+ var currentOffsetY by remember { mutableFloatStateOf(offsetY) }
748+
749+ LaunchedEffect (scale, offsetX, offsetY) {
750+ currentScale = scale
751+ currentOffsetX = offsetX
752+ currentOffsetY = offsetY
753+ }
754+
755+ val imageRequest = remember(imageUri, useOriginalSize) {
756+ ImageRequest .Builder (context)
757+ .data(imageUri)
758+ .apply {
759+ if (useOriginalSize) {
760+ size(Size .ORIGINAL )
761+ memoryCacheKey(imageUri.toString() + " _original" )
762+ }
763+ }
764+ .build()
765+ }
766+
767+ Box (
768+ modifier = modifier
769+ .pointerInput(Unit ) {
770+ detectTransformGestures { centroid, pan, zoom, rotation ->
771+ val newScale = (currentScale * zoom).coerceIn(1f , 5f )
772+
773+ val newOffsetX = currentOffsetX + pan.x
774+ val newOffsetY = currentOffsetY + pan.y
775+
776+ currentScale = newScale
777+ currentOffsetX = newOffsetX
778+ currentOffsetY = newOffsetY
779+
780+ onTransform(newScale, newOffsetX, newOffsetY)
781+ }
782+ }
783+ ) {
784+ AsyncImage (
785+ model = imageRequest,
786+ contentDescription = contentDescription,
787+ modifier = Modifier
788+ .fillMaxSize()
789+ .graphicsLayer(
790+ scaleX = currentScale,
791+ scaleY = currentScale,
792+ translationX = currentOffsetX,
793+ translationY = currentOffsetY
794+ ),
795+ contentScale = ContentScale .Fit
796+ )
797+ }
798+ }
0 commit comments