1+ package com.smarttoolfactory.image.zoom
2+
3+ import androidx.compose.animation.core.Animatable
4+ import androidx.compose.animation.core.VectorConverter
5+ import androidx.compose.animation.core.spring
6+ import androidx.compose.foundation.gestures.detectTapGestures
7+ import androidx.compose.runtime.*
8+ import androidx.compose.ui.Modifier
9+ import androidx.compose.ui.composed
10+ import androidx.compose.ui.geometry.Offset
11+ import androidx.compose.ui.geometry.Size
12+ import androidx.compose.ui.graphics.graphicsLayer
13+ import androidx.compose.ui.input.pointer.pointerInput
14+ import androidx.compose.ui.layout.onSizeChanged
15+ import androidx.compose.ui.unit.toSize
16+ import com.smarttoolfactory.gesture.detectTransformGestures
17+ import com.smarttoolfactory.image.transform.Transform
18+ import kotlinx.coroutines.launch
19+
20+ /* *
21+ * Modifier that zooms in or out of Composable set to.
22+ * @param keys are used for [Modifier.pointerInput] to restart closure when any keys assigned
23+ * change
24+ * @param initialZoom zoom set initially
25+ * @param minZoom minimum zoom value
26+ * @param maxZoom maximum zoom value
27+ */
28+ fun Modifier.zoom (
29+ vararg keys : Any? ,
30+ initialZoom : Float = 1f,
31+ minZoom : Float = 1f,
32+ maxZoom : Float = 5f,
33+ clip : Boolean = true,
34+ onChange : (Transform ) -> Unit = {}
35+ ) = composed(
36+ factory = {
37+
38+ val coroutineScope = rememberCoroutineScope()
39+ val zoomMin = minZoom.coerceAtLeast(.5f )
40+ val zoomMax = maxZoom.coerceAtLeast(1f )
41+ val zoomInitial = initialZoom.coerceIn(zoomMin, zoomMax)
42+
43+ require(zoomMax >= zoomMin)
44+
45+ var size by remember { mutableStateOf(Size .Zero ) }
46+
47+
48+ val animatableOffset = remember {
49+ Animatable (Offset .Zero , Offset .VectorConverter )
50+ }
51+ val animatableZoom = remember { Animatable (zoomInitial) }
52+
53+ Modifier
54+ // .then(if (clip) Modifier.clipToBounds() else Modifier)
55+ .graphicsLayer {
56+ val zoom = animatableZoom.value
57+ translationX = animatableOffset.value.x
58+ translationY = animatableOffset.value.y
59+ scaleX = zoom
60+ scaleY = zoom
61+ this .clip = clip
62+
63+ onChange(Transform (translationX, translationY, scaleX, scaleY))
64+ }
65+ .pointerInput(keys) {
66+
67+ detectTransformGestures(
68+ onGesture = { _,
69+ gesturePan: Offset ,
70+ gestureZoom: Float ,
71+ _,
72+ _,
73+ _ ->
74+
75+ var zoom = animatableZoom.value
76+ val offset = animatableOffset.value
77+
78+ zoom = (zoom * gestureZoom).coerceIn(zoomMin, zoomMax)
79+ val newOffset = offset + gesturePan.times(zoom)
80+
81+ val maxX = (size.width * (zoom - 1 ) / 2f ).coerceAtLeast(0f )
82+ val maxY = (size.height * (zoom - 1 ) / 2f ).coerceAtLeast(0f )
83+
84+ coroutineScope.launch {
85+ animatableZoom.snapTo(zoom)
86+ }
87+ coroutineScope.launch {
88+ animatableOffset.snapTo(
89+ Offset (
90+ newOffset.x.coerceIn(- maxX, maxX),
91+ newOffset.y.coerceIn(- maxY, maxY)
92+ )
93+ )
94+ }
95+ }
96+ )
97+ }
98+ .pointerInput(keys) {
99+ detectTapGestures(
100+ onDoubleTap = {
101+ coroutineScope.launch {
102+ animatableOffset.animateTo(Offset .Zero , spring())
103+ }
104+ coroutineScope.launch {
105+ animatableZoom.animateTo(zoomInitial, spring())
106+ }
107+ }
108+ )
109+ }
110+ .onSizeChanged {
111+ size = it.toSize()
112+ }
113+ },
114+ inspectorInfo = {
115+
116+ }
117+ )
0 commit comments