Skip to content

Commit acf3f49

Browse files
Update PlantDetailView.kt
android#868 : made image in detail screen zoomable
1 parent 705e962 commit acf3f49

File tree

1 file changed

+164
-3
lines changed

1 file changed

+164
-3
lines changed

app/src/main/java/com/google/samples/apps/sunflower/compose/plantdetail/PlantDetailView.kt

+164-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import androidx.compose.foundation.Image
2727
import androidx.compose.foundation.ScrollState
2828
import androidx.compose.foundation.background
2929
import androidx.compose.foundation.clickable
30+
import androidx.compose.foundation.gestures.detectTapGestures
31+
import androidx.compose.foundation.gestures.detectTransformGestures
32+
import androidx.compose.foundation.gestures.draggable
3033
import androidx.compose.foundation.layout.Arrangement
3134
import androidx.compose.foundation.layout.Box
3235
import androidx.compose.foundation.layout.Column
@@ -58,13 +61,20 @@ import androidx.compose.runtime.Composable
5861

5962
import androidx.compose.runtime.getValue
6063
import androidx.compose.runtime.livedata.observeAsState
64+
import androidx.compose.runtime.mutableFloatStateOf
6165
import androidx.compose.runtime.mutableStateOf
6266
import androidx.compose.runtime.remember
6367
import androidx.compose.runtime.setValue
6468
import androidx.compose.ui.Alignment
6569
import androidx.compose.ui.Modifier
6670
import androidx.compose.ui.draw.alpha
71+
import androidx.compose.ui.draw.clip
72+
import androidx.compose.ui.draw.scale
73+
import androidx.compose.ui.geometry.Offset
6774
import androidx.compose.ui.graphics.Color
75+
import androidx.compose.ui.graphics.RectangleShape
76+
import androidx.compose.ui.graphics.graphicsLayer
77+
import androidx.compose.ui.input.pointer.pointerInput
6878
import androidx.compose.ui.layout.ContentScale
6979
import androidx.compose.ui.layout.onGloballyPositioned
7080
import androidx.compose.ui.layout.positionInWindow
@@ -77,6 +87,7 @@ import androidx.compose.ui.semantics.semantics
7787
import androidx.compose.ui.text.font.FontWeight
7888
import androidx.compose.ui.tooling.preview.Preview
7989
import androidx.compose.ui.unit.Dp
90+
import androidx.compose.ui.unit.IntSize
8091
import androidx.compose.ui.unit.dp
8192
import androidx.compose.ui.viewinterop.AndroidViewBinding
8293
import androidx.constraintlayout.compose.ConstraintLayout
@@ -97,6 +108,7 @@ import com.google.samples.apps.sunflower.data.Plant
97108
import com.google.samples.apps.sunflower.databinding.ItemPlantDescriptionBinding
98109
import com.google.samples.apps.sunflower.ui.SunflowerTheme
99110
import com.google.samples.apps.sunflower.viewmodels.PlantDetailViewModel
111+
import kotlin.math.max
100112

101113
/**
102114
* As these callbacks are passed in through multiple Composables, to avoid having to name
@@ -267,6 +279,100 @@ private fun PlantDetailsContent(
267279
}
268280
}
269281

282+
//@OptIn(ExperimentalGlideComposeApi::class)
283+
//@Composable
284+
//private fun PlantImage(
285+
// imageUrl: String,
286+
// imageHeight: Dp,
287+
// modifier: Modifier = Modifier,
288+
// placeholderColor: Color = MaterialTheme.colorScheme.onSurface.copy(0.2f)
289+
//) {
290+
// var isLoading by remember { mutableStateOf(true) }
291+
// // Define mutable state variables to keep track of the scale and offset.
292+
// var scale by remember { androidx.compose.runtime.mutableFloatStateOf(1f) }
293+
// var offset by remember { mutableStateOf(Offset(0f, 0f)) }
294+
// Box(
295+
// modifier
296+
// .fillMaxWidth()
297+
// .height(imageHeight)
298+
// ) {
299+
// if (isLoading) {
300+
// // TODO: Update this implementation once Glide releases a version
301+
// // that contains this feature: https://github.com/bumptech/glide/pull/4934
302+
// Box(
303+
// Modifier
304+
// .fillMaxSize()
305+
// .background(placeholderColor)
306+
// )
307+
// }
308+
// GlideImage(
309+
// model = imageUrl,
310+
// contentDescription = null,
311+
// modifier = Modifier
312+
// .fillMaxWidth()
313+
// .height(imageHeight)
314+
// .pointerInput(Unit) {
315+
// detectTransformGestures { _, pan, zoom, _ ->
316+
// val newScale = (scale * zoom).coerceIn(1f, 3f)
317+
//
318+
// // Calculate new offset
319+
// val newOffset = if (newScale == 1f) Offset(0f, 0f) else offset + pan
320+
//
321+
// // Get the size of the box
322+
// val boxSize = this.size
323+
//
324+
// // Calculate the size of the image based on the new scale
325+
// val imageWidth = boxSize.width * newScale
326+
// val imageHeight = boxSize.height * newScale
327+
//
328+
// // Ensure the image does not move out of the box boundaries
329+
// val maxX = (imageWidth - boxSize.width) / 2
330+
// val maxY = (imageHeight - boxSize.height) / 2
331+
// val clampedOffset = Offset(
332+
// newOffset.x.coerceIn(-maxX, maxX),
333+
// newOffset.y.coerceIn(-maxY, maxY)
334+
// )
335+
//
336+
// scale = newScale
337+
// offset = clampedOffset
338+
// }
339+
// }
340+
// .graphicsLayer(
341+
// scaleX = scale,
342+
// scaleY = scale,
343+
// translationX = offset.x,
344+
// translationY = offset.y
345+
// ),
346+
// contentScale = ContentScale.Crop,
347+
// ) {
348+
// it.addListener(object : RequestListener<Drawable> {
349+
// override fun onLoadFailed(
350+
// e: GlideException?,
351+
// model: Any?,
352+
// target: Target<Drawable>,
353+
// isFirstResource: Boolean
354+
// ): Boolean {
355+
// isLoading = false
356+
// return false
357+
// }
358+
//
359+
// override fun onResourceReady(
360+
// resource: Drawable,
361+
// model: Any,
362+
// target: Target<Drawable>?,
363+
// dataSource: DataSource,
364+
// isFirstResource: Boolean
365+
// ): Boolean {
366+
// isLoading = false
367+
// return false
368+
// }
369+
// })
370+
// }
371+
// }
372+
//}
373+
374+
375+
270376
@OptIn(ExperimentalGlideComposeApi::class)
271377
@Composable
272378
private fun PlantImage(
@@ -276,14 +382,58 @@ private fun PlantImage(
276382
placeholderColor: Color = MaterialTheme.colorScheme.onSurface.copy(0.2f)
277383
) {
278384
var isLoading by remember { mutableStateOf(true) }
385+
var scale by remember { mutableFloatStateOf(1f) }
386+
var imageSize by remember { mutableStateOf(IntSize.Zero) }
387+
var offset by remember { mutableStateOf(Offset.Zero) }
388+
279389
Box(
280390
modifier
391+
.clip(RectangleShape) // Clip the box content
281392
.fillMaxWidth()
282393
.height(imageHeight)
394+
.pointerInput(Unit) {
395+
detectTransformGestures { _, pan, zoom, _ ->
396+
// Calculate the new scale value, ensuring it stays within the allowed range
397+
scale = (scale * zoom).coerceIn(1f, 3f)
398+
399+
// Calculate the new offset by adding the pan gesture delta to the current offset
400+
val newOffset = offset + pan
401+
402+
403+
// Calculate max movement within the box
404+
val maxX = if (imageSize.width * scale > size.width) {
405+
(imageSize.width * scale - size.width) / 2
406+
} else {
407+
0f
408+
}
409+
410+
val maxY = if (imageSize.height * scale > size.height) {
411+
(imageSize.height * scale - size.height) / 2
412+
} else {
413+
0f
414+
}
415+
416+
// Update the offset, clamping it to the calculated bounds
417+
offset = Offset(
418+
x = newOffset.x.coerceIn(-maxX, maxX),
419+
y = newOffset.y.coerceIn(-maxY, maxY)
420+
)
421+
}
422+
}
423+
.pointerInput(Unit) {
424+
detectTapGestures(
425+
onDoubleTap = {
426+
if (scale > 1f) {
427+
scale = 1f
428+
offset = Offset.Zero
429+
} else {
430+
scale = 2f
431+
}
432+
}
433+
)
434+
}
283435
) {
284436
if (isLoading) {
285-
// TODO: Update this implementation once Glide releases a version
286-
// that contains this feature: https://github.com/bumptech/glide/pull/4934
287437
Box(
288438
Modifier
289439
.fillMaxSize()
@@ -294,7 +444,17 @@ private fun PlantImage(
294444
model = imageUrl,
295445
contentDescription = null,
296446
modifier = Modifier
297-
.fillMaxSize(),
447+
.fillMaxSize()
448+
.graphicsLayer(
449+
// Set the horizontal scaling factor, limiting zoom between 50% and 300%
450+
scaleX = maxOf(0.5f, minOf(3f, scale)),
451+
// Set the vertical scaling factor, limiting zoom between 50% and 300%
452+
scaleY = maxOf(0.5f, minOf(3f, scale)),
453+
//Set the horizontal translation (panning) based on the calculated offset
454+
translationX = offset.x,
455+
// Set the vertical translation (panning) based on the calculated offset
456+
translationY = offset.y
457+
),
298458
contentScale = ContentScale.Crop,
299459
) {
300460
it.addListener(object : RequestListener<Drawable> {
@@ -316,6 +476,7 @@ private fun PlantImage(
316476
isFirstResource: Boolean
317477
): Boolean {
318478
isLoading = false
479+
imageSize = IntSize(resource.intrinsicWidth, resource.intrinsicHeight)
319480
return false
320481
}
321482
})

0 commit comments

Comments
 (0)