Skip to content

Commit b74ba42

Browse files
add double tap and conditional enable for zoom
zoomOnDoubleTap param for Modifier.enhanced and Modifier.animatedZoom adds option to implement three levels of zoom on double tap onGestureStart param for Modifier.enhanced and Modifier.animatedZoom adds option to enable pan conditionally
1 parent 4b5a61e commit b74ba42

File tree

10 files changed

+294
-94
lines changed

10 files changed

+294
-94
lines changed

image/src/main/java/com/smarttoolfactory/image/util/ZoomUtil.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ internal fun calculateZoom(
1818
val newZoom: Float
1919

2020
when (zoomLevel) {
21-
ZoomLevel.Initial -> {
21+
ZoomLevel.Mid -> {
2222
newZoomLevel = ZoomLevel.Max
2323
newZoom = max.coerceAtMost(3f)
2424
}
@@ -27,13 +27,25 @@ internal fun calculateZoom(
2727
newZoom = if (min == initial) (min + max.coerceAtMost(3f)) / 2 else min
2828
}
2929
else -> {
30-
newZoomLevel = ZoomLevel.Initial
30+
newZoomLevel = ZoomLevel.Mid
3131
newZoom = initial.coerceAtMost(2f)
3232
}
3333
}
3434
return Pair(newZoomLevel, newZoom)
3535
}
3636

37+
internal fun getNextZoomLevel(zoomLevel: ZoomLevel): ZoomLevel = when (zoomLevel) {
38+
ZoomLevel.Mid -> {
39+
ZoomLevel.Max
40+
}
41+
ZoomLevel.Max -> {
42+
ZoomLevel.Min
43+
}
44+
else -> {
45+
ZoomLevel.Mid
46+
}
47+
}
48+
3749
/**
3850
* Update graphic layer with [zoomState]
3951
*/

image/src/main/java/com/smarttoolfactory/image/zoom/AnimatedZoomLayout.kt

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize
66
import androidx.compose.runtime.Composable
77
import androidx.compose.ui.Alignment
88
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.geometry.Offset
910
import androidx.compose.ui.graphics.Color
1011
import androidx.compose.ui.layout.Measurable
1112
import androidx.compose.ui.layout.Placeable
@@ -19,13 +20,42 @@ import com.smarttoolfactory.image.SlotsEnum
1920

2021
/**
2122
* Layout that can zoom, rotate, pan its content with fling and moving back to bounds animation.
23+
* @param clip when set to true clips to parent bounds. Anything outside parent bounds is not
24+
* drawn
25+
* @param minZoom minimum zoom value
26+
* @param maxZoom maximum zoom value
27+
* @param fling when set to true dragging pointer builds up velocity. When last
28+
* pointer leaves Composable a movement invoked against friction till velocity drops below
29+
* to threshold
30+
* @param moveToBounds when set to true if image zoom is lower than initial zoom or
31+
* panned out of image boundaries moves back to bounds with animation.
32+
* @param zoomable when set to true zoom is enabled
33+
* @param pannable when set to true pan is enabled
34+
* @param rotatable when set to true rotation is enabled
35+
* @param limitPan limits pan to bounds of parent Composable. Using this flag prevents creating
36+
* empty space on sides or edges of parent
37+
* @param zoomOnDoubleTap lambda that returns current [ZoomLevel] and based on current level
38+
* enables developer to define zoom on double tap gesture
39+
* @param enabled lambda can be used selectively enable or disable pan and intercepting with
40+
* scroll, drag or lists or pagers using current zoom, pan or rotation values
2241
*/
2342
@Composable
2443
fun AnimatedZoomLayout(
2544
modifier: Modifier = Modifier,
45+
clip: Boolean = true,
46+
initialZoom: Float = 1f,
47+
minZoom: Float = 1f,
48+
maxZoom: Float = 3f,
49+
fling: Boolean = true,
50+
moveToBounds: Boolean = false,
51+
zoomable: Boolean = true,
52+
pannable: Boolean = true,
53+
rotatable: Boolean = false,
54+
limitPan: Boolean = true,
55+
enabled: (Float, Offset, Float) -> Boolean = DefaultEnabled,
56+
zoomOnDoubleTap: (ZoomLevel) -> Float = DefaultOnDoubleTap,
2657
content: @Composable () -> Unit
2758
) {
28-
2959
AnimatedZoomSubcomposeLayout(
3060
modifier = modifier,
3161
mainContent = { content() }
@@ -35,9 +65,19 @@ fun AnimatedZoomLayout(
3565
.fillMaxSize()
3666
.border(5.dp, Color.Red)
3767
.animatedZoom(
68+
enabled = enabled,
69+
clip = clip,
70+
zoomOnDoubleTap = zoomOnDoubleTap,
3871
animatedZoomState = rememberAnimatedZoomState(
39-
minZoom = .5f,
40-
maxZoom = 30f,
72+
minZoom = minZoom,
73+
maxZoom = maxZoom,
74+
initialZoom = initialZoom,
75+
fling = fling,
76+
moveToBounds = moveToBounds,
77+
zoomable = zoomable,
78+
pannable = pannable,
79+
rotatable = rotatable,
80+
limitPan = limitPan,
4181
contentSize = it
4282
),
4383
),

image/src/main/java/com/smarttoolfactory/image/zoom/AnimatedZoomModifier.kt

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,57 @@ import androidx.compose.runtime.rememberCoroutineScope
55
import androidx.compose.ui.Modifier
66
import androidx.compose.ui.composed
77
import androidx.compose.ui.draw.clipToBounds
8+
import androidx.compose.ui.geometry.Offset
89
import androidx.compose.ui.graphics.graphicsLayer
910
import androidx.compose.ui.input.pointer.pointerInput
1011
import com.smarttoolfactory.gesture.detectTransformGestures
12+
import com.smarttoolfactory.image.util.getNextZoomLevel
1113
import com.smarttoolfactory.image.util.update
1214
import kotlinx.coroutines.launch
1315

1416
/**
15-
* Modifier that zooms in or out of Composable set to. This zoom modifier has option
16-
* to move back to bounds with an animation or option to have fling gesture when user removes
17-
* from screen while velocity is higher than threshold to have smooth touch effect.
18-
*
19-
* @param key is used for [Modifier.pointerInput] to restart closure when any keys assigned
20-
* change
21-
* @param consume flag to prevent other gestures such as scroll, drag or transform to get
22-
* @param clip when set to true clips to parent bounds. Anything outside parent bounds is not
23-
* drawn
24-
* empty space on sides or edges of parent.
25-
* [EnhancedZoomData] of this modifier
26-
*/
17+
* Modifier that zooms in or out of Composable set to. This zoom modifier has option
18+
* to move back to bounds with an animation or option to have fling gesture when user removes
19+
* from screen while velocity is higher than threshold to have smooth touch effect.
20+
*
21+
* @param key is used for [Modifier.pointerInput] to restart closure when any keys assigned
22+
* change
23+
* @param clip when set to true clips to parent bounds. Anything outside parent bounds is not
24+
* drawn
25+
* @param animatedZoomState State of the zoom that contains option to set initial, min, max zoom,
26+
* enabling rotation, pan or zoom
27+
* @param zoomOnDoubleTap lambda that returns current [ZoomLevel] and based on current level
28+
* enables developer to define zoom on double tap gesture
29+
* @param enabled lambda can be used selectively enable or disable pan and intercepting with
30+
* scroll, drag or lists or pagers using current zoom, pan or rotation values
31+
*/
2732
fun Modifier.animatedZoom(
2833
key: Any? = Unit,
29-
consume: Boolean = true,
3034
clip: Boolean = true,
3135
animatedZoomState: AnimatedZoomState,
36+
enabled: (Float, Offset, Float) -> Boolean = DefaultEnabled,
37+
zoomOnDoubleTap: (ZoomLevel) -> Float = animatedZoomState.DefaultOnDoubleTap,
3238
) = composed(
3339

3440
factory = {
3541

3642
val coroutineScope = rememberCoroutineScope()
3743

44+
// Current Zoom level
45+
var zoomLevel = ZoomLevel.Min
46+
47+
// Whether panning should be limited to bounds of gesture area or not
3848
val boundPan = animatedZoomState.limitPan && !animatedZoomState.rotatable
49+
50+
// If we bound to touch area or clip is true Modifier.clipToBounds is used
3951
val clipToBounds = (clip || boundPan)
4052

4153
val transformModifier = Modifier.pointerInput(key) {
4254
// Pass size of this Composable this Modifier is attached for constraining operations
4355
// inside this bounds
4456
animatedZoomState.size = this.size
4557
detectTransformGestures(
46-
consume = consume,
58+
consume = false,
4759
onGestureEnd = {
4860
coroutineScope.launch {
4961
animatedZoomState.onGestureEnd {
@@ -52,15 +64,24 @@ fun Modifier.animatedZoom(
5264
},
5365
onGesture = { centroid, pan, zoom, rotate, mainPointer, pointerList ->
5466

67+
val currentZoom = animatedZoomState.zoom
68+
val currentPan = animatedZoomState.pan
69+
val currentRotation = animatedZoomState.rotation
70+
val gestureEnabled = enabled(currentZoom, currentPan, currentRotation)
71+
5572
coroutineScope.launch {
5673
animatedZoomState.onGesture(
5774
centroid = centroid,
58-
pan = pan,
75+
pan = if (gestureEnabled) pan else Offset.Zero,
5976
zoom = zoom,
6077
rotation = rotate,
6178
mainPointer = mainPointer,
6279
changes = pointerList
6380
)
81+
82+
if (gestureEnabled) {
83+
mainPointer.consume()
84+
}
6485
}
6586
}
6687
)
@@ -73,7 +94,9 @@ fun Modifier.animatedZoom(
7394
detectTapGestures(
7495
onDoubleTap = {
7596
coroutineScope.launch {
76-
animatedZoomState.onDoubleTap {}
97+
val newZoom = zoomOnDoubleTap(zoomLevel)
98+
zoomLevel = getNextZoomLevel(zoomLevel)
99+
animatedZoomState.onDoubleTap(zoom = newZoom) {}
77100
}
78101
}
79102
)

image/src/main/java/com/smarttoolfactory/image/zoom/AnimatedZoomStateImpl.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ open class AnimatedZoomState constructor(
4040

4141
val maxX = ((contentWidth * zoom - size.width) / 2f).coerceAtLeast(0f)
4242
val maxY = ((contentHeight * zoom - size.height) / 2f).coerceAtLeast(0f)
43-
44-
println("🎾 GetBounds zoom:$zoom, size: $size, maxX: $maxX, maxY: $maxY")
4543
return Offset(maxX, maxY)
4644
}
4745
}

0 commit comments

Comments
 (0)