Skip to content

Commit 6f7c2d6

Browse files
add vertical thumb movement to BeforeAfterImage
1 parent 5882393 commit 6f7c2d6

File tree

1 file changed

+60
-37
lines changed

1 file changed

+60
-37
lines changed

image/src/main/java/com/smarttoolfactory/image/beforeafter/BeforeAfterImage.kt

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes
44
import androidx.annotation.FloatRange
55
import androidx.compose.foundation.Canvas
66
import androidx.compose.foundation.background
7+
import androidx.compose.foundation.gestures.detectTapGestures
78
import androidx.compose.foundation.layout.*
89
import androidx.compose.foundation.shape.CircleShape
910
import androidx.compose.material.Icon
@@ -33,6 +34,9 @@ import com.smarttoolfactory.image.R
3334
import com.smarttoolfactory.image.getParentSize
3435
import com.smarttoolfactory.image.getScaledBitmapRect
3536
import 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

Comments
 (0)