@@ -27,6 +27,9 @@ import androidx.compose.foundation.Image
27
27
import androidx.compose.foundation.ScrollState
28
28
import androidx.compose.foundation.background
29
29
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
30
33
import androidx.compose.foundation.layout.Arrangement
31
34
import androidx.compose.foundation.layout.Box
32
35
import androidx.compose.foundation.layout.Column
@@ -58,13 +61,20 @@ import androidx.compose.runtime.Composable
58
61
59
62
import androidx.compose.runtime.getValue
60
63
import androidx.compose.runtime.livedata.observeAsState
64
+ import androidx.compose.runtime.mutableFloatStateOf
61
65
import androidx.compose.runtime.mutableStateOf
62
66
import androidx.compose.runtime.remember
63
67
import androidx.compose.runtime.setValue
64
68
import androidx.compose.ui.Alignment
65
69
import androidx.compose.ui.Modifier
66
70
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
67
74
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
68
78
import androidx.compose.ui.layout.ContentScale
69
79
import androidx.compose.ui.layout.onGloballyPositioned
70
80
import androidx.compose.ui.layout.positionInWindow
@@ -77,6 +87,7 @@ import androidx.compose.ui.semantics.semantics
77
87
import androidx.compose.ui.text.font.FontWeight
78
88
import androidx.compose.ui.tooling.preview.Preview
79
89
import androidx.compose.ui.unit.Dp
90
+ import androidx.compose.ui.unit.IntSize
80
91
import androidx.compose.ui.unit.dp
81
92
import androidx.compose.ui.viewinterop.AndroidViewBinding
82
93
import androidx.constraintlayout.compose.ConstraintLayout
@@ -97,6 +108,7 @@ import com.google.samples.apps.sunflower.data.Plant
97
108
import com.google.samples.apps.sunflower.databinding.ItemPlantDescriptionBinding
98
109
import com.google.samples.apps.sunflower.ui.SunflowerTheme
99
110
import com.google.samples.apps.sunflower.viewmodels.PlantDetailViewModel
111
+ import kotlin.math.max
100
112
101
113
/* *
102
114
* As these callbacks are passed in through multiple Composables, to avoid having to name
@@ -267,6 +279,100 @@ private fun PlantDetailsContent(
267
279
}
268
280
}
269
281
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
+
270
376
@OptIn(ExperimentalGlideComposeApi ::class )
271
377
@Composable
272
378
private fun PlantImage (
@@ -276,14 +382,58 @@ private fun PlantImage(
276
382
placeholderColor : Color = MaterialTheme .colorScheme.onSurface.copy(0.2f)
277
383
) {
278
384
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
+
279
389
Box (
280
390
modifier
391
+ .clip(RectangleShape ) // Clip the box content
281
392
.fillMaxWidth()
282
393
.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
+ }
283
435
) {
284
436
if (isLoading) {
285
- // TODO: Update this implementation once Glide releases a version
286
- // that contains this feature: https://github.com/bumptech/glide/pull/4934
287
437
Box (
288
438
Modifier
289
439
.fillMaxSize()
@@ -294,7 +444,17 @@ private fun PlantImage(
294
444
model = imageUrl,
295
445
contentDescription = null ,
296
446
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
+ ),
298
458
contentScale = ContentScale .Crop ,
299
459
) {
300
460
it.addListener(object : RequestListener <Drawable > {
@@ -316,6 +476,7 @@ private fun PlantImage(
316
476
isFirstResource : Boolean
317
477
): Boolean {
318
478
isLoading = false
479
+ imageSize = IntSize (resource.intrinsicWidth, resource.intrinsicHeight)
319
480
return false
320
481
}
321
482
})
0 commit comments