@@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes
44import androidx.annotation.FloatRange
55import androidx.compose.foundation.Canvas
66import androidx.compose.foundation.background
7+ import androidx.compose.foundation.gestures.detectTapGestures
78import androidx.compose.foundation.layout.*
89import androidx.compose.foundation.shape.CircleShape
910import androidx.compose.material.Icon
@@ -33,6 +34,9 @@ import com.smarttoolfactory.image.R
3334import com.smarttoolfactory.image.getParentSize
3435import com.smarttoolfactory.image.getScaledBitmapRect
3536import com.smarttoolfactory.image.util.scale
37+ import com.smarttoolfactory.image.util.update
38+ import com.smarttoolfactory.image.zoom.rememberZoomState
39+ import kotlinx.coroutines.launch
3640
3741/* *
3842 * A composable that lays out and draws a given [beforeImage] and [afterImage] at given [order]
@@ -45,6 +49,7 @@ import com.smarttoolfactory.image.util.scale
4549 * @param afterImage image that show final progress
4650 * @param enableProgressWithTouch flag to enable drag and change progress with touch
4751 * @param enableZoom when enabled images are zoomable and pannable
52+ * @param verticalThumbMove when set to true thumb moves to touch position in y coordinate
4853 * @param order order of images to be drawn
4954 * @param alignment determines where image will be aligned inside [BoxWithConstraints]
5055 * This is observable when bitmap image/width ratio differs from [Canvas] that draws [ImageBitmap]
@@ -62,6 +67,7 @@ fun BeforeAfterImage(
6267 afterImage : ImageBitmap ,
6368 enableProgressWithTouch : Boolean = true,
6469 enableZoom : Boolean = true,
70+ verticalThumbMove : Boolean = false,
6571 order : Order = Order .BeforeAfter ,
6672 lineColor : Color = Color .White ,
6773 @DrawableRes thumbResource : Int = R .drawable.baseline_swap_horiz_24,
@@ -71,9 +77,7 @@ fun BeforeAfterImage(
7177 alignment : Alignment = Alignment .Center ,
7278 contentDescription : String? = null,
7379) {
74-
7580 val density = LocalDensity .current
76- val thumbPosition = thumbPositionPercent.coerceIn(0f , 100f )
7781
7882 BeforeAfterImage (
7983 modifier = modifier,
@@ -87,27 +91,38 @@ fun BeforeAfterImage(
8791 contentDescription = contentDescription,
8892 ) {
8993
90- val handlePosition = position.x
91- val posY: Int
92-
93- val realPos = handlePosition - with (density) {
94- val thumbRadius = (thumbSize / 2 )
94+ var positionX = position.x
95+ var positionY = position.y
9596
96- posY = ((imageHeight * thumbPosition / 100f - thumbRadius) - imageHeight / 2 )
97- .roundToPx()
97+ val linePosition: Float
9898
99- imageWidth.toPx() / 2
99+ with (density) {
100+ val thumbRadius = (thumbSize / 2 ).toPx()
101+ val imageWidthInPx = imageWidth.toPx()
102+ val imageHeightInPx = imageHeight.toPx()
103+
104+ linePosition = positionX.coerceIn(0f , imageWidthInPx)
105+ positionX - = imageWidth.toPx() / 2
106+
107+ positionY = if (verticalThumbMove) {
108+ (positionY - imageHeightInPx / 2 )
109+ .coerceIn(
110+ - imageHeightInPx / 2 + thumbRadius,
111+ imageHeightInPx / 2 - thumbRadius
112+ )
113+ } else {
114+ val thumbPosition = thumbPositionPercent.coerceIn(0f , 100f )
115+ ((imageHeightInPx * thumbPosition / 100f - thumbRadius) - imageHeightInPx / 2 )
116+ }
100117 }
101118
102119 Canvas (modifier = Modifier .size(imageWidth, imageHeight)) {
103- val canvasWidth = size.width
104- val imagePosition = handlePosition.coerceIn(0f , canvasWidth)
105120
106121 drawLine(
107122 lineColor,
108123 strokeWidth = 1.5 .dp.toPx(),
109- start = Offset (imagePosition , 0f ),
110- end = Offset (imagePosition , size.height)
124+ start = Offset (linePosition , 0f ),
125+ end = Offset (linePosition , size.height)
111126 )
112127 }
113128
@@ -117,7 +132,7 @@ fun BeforeAfterImage(
117132 tint = Color .Gray ,
118133 modifier = Modifier
119134 .offset {
120- IntOffset (realPos .toInt(), posY )
135+ IntOffset (positionX .toInt(), positionY.toInt() )
121136 }
122137 .shadow(2 .dp, CircleShape )
123138 .background(Color .White )
@@ -199,7 +214,7 @@ fun BeforeAfterImage(
199214/* *
200215 * A composable that lays out and draws a given [beforeImage] and [afterImage] at given [order]
201216 * with specified [contentScale] and returns draw area and section of drawn bitmap.
202- *
217+ *
203218 * [BeforeAfterImageScope] extends [ImageScope] that returns draw area dimensions and image draw rect
204219 * and touch position of user on screen.
205220 *
@@ -302,23 +317,20 @@ fun BeforeAfterImage(
302317
303318 var isHandleTouched by remember { mutableStateOf(false ) }
304319
305- var zoom by remember { mutableStateOf( 1f ) }
306- var pan by remember { mutableStateOf( Offset . Zero ) }
320+ val zoomState = rememberZoomState(limitPan = true )
321+ val coroutineScope = rememberCoroutineScope()
307322
308323 val transformModifier = Modifier .pointerInput(Unit ) {
309324 detectTransformGestures(
310325 onGesture = { _: Offset , panChange: Offset , zoomChange: Float , _, _, _ ->
311326
312- zoom = (zoom * zoomChange).coerceIn(1f , 5f )
313-
314- val maxX = (size.width * (zoom - 1 ) / 2f )
315- val maxY = (size.height * (zoom - 1 ) / 2f )
316-
317- val newPan = pan + panChange.times(zoom)
318- pan = Offset (
319- newPan.x.coerceIn(- maxX, maxX),
320- newPan.y.coerceIn(- maxY, maxY)
321- )
327+ coroutineScope.launch {
328+ zoomState.updateZoomState(
329+ size,
330+ gesturePan = panChange,
331+ gestureZoom = zoomChange
332+ )
333+ }
322334 }
323335 )
324336 }
@@ -330,11 +342,11 @@ fun BeforeAfterImage(
330342 val xPos = position.x
331343
332344 isHandleTouched =
333- ((rawOffset.x - xPos) * (rawOffset.x - xPos) < 10000 )
345+ ((rawOffset.x - xPos) * (rawOffset.x - xPos) < 5000 )
334346 },
335347 onMove = {
336348 if (isHandleTouched) {
337- rawOffset = rawOffset.copy(x = it.position.x)
349+ rawOffset = it.position
338350 onProgressChange?.invoke(
339351 scaleToUserValue(rawOffset.x)
340352 )
@@ -347,16 +359,27 @@ fun BeforeAfterImage(
347359 )
348360 }
349361
362+ val tapModifier = Modifier .pointerInput(Unit ) {
363+ detectTapGestures(
364+ onDoubleTap = {
365+ coroutineScope.launch {
366+ zoomState.animatePanTo(Offset .Zero )
367+ }
368+
369+ coroutineScope.launch {
370+ zoomState.animateZoomTo(1f )
371+ }
372+ }
373+ )
374+ }
375+
350376 val graphicsModifier = Modifier .graphicsLayer {
351- this .scaleX = zoom
352- this .scaleY = zoom
353- this .translationX = pan.x
354- this .translationY = pan.y
377+ this .update(zoomState)
355378 }
356379
357380 val imageModifier = Modifier
358381 .clipToBounds()
359- .then(if (enableZoom) transformModifier else Modifier )
382+ .then(if (enableZoom) transformModifier.then(tapModifier) else Modifier )
360383 .then(if (enableProgressWithTouch) touchModifier else Modifier )
361384 .then(graphicsModifier)
362385
@@ -385,8 +408,8 @@ fun BeforeAfterImage(
385408 beforeImage = beforeImage,
386409 afterImage = afterImage,
387410 position = rawOffset,
388- translateX = pan.x,
389- zoom = zoom,
411+ translateX = zoomState. pan.x,
412+ zoom = zoomState. zoom,
390413 bitmapRect = bitmapRect,
391414 imageWidth = imageWidth,
392415 imageHeight = imageHeight,
0 commit comments