Skip to content

Commit 34fe2b0

Browse files
add TransformLayout
1 parent 2f86574 commit 34fe2b0

File tree

1 file changed

+250
-0
lines changed

1 file changed

+250
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
package com.smarttoolfactory.image.transform
2+
3+
import androidx.compose.foundation.border
4+
import androidx.compose.foundation.layout.*
5+
import androidx.compose.runtime.*
6+
import androidx.compose.ui.Alignment
7+
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.draw.clipToBounds
9+
import androidx.compose.ui.geometry.Offset
10+
import androidx.compose.ui.geometry.Rect
11+
import androidx.compose.ui.geometry.Size
12+
import androidx.compose.ui.graphics.Color
13+
import androidx.compose.ui.graphics.graphicsLayer
14+
import androidx.compose.ui.input.pointer.pointerInput
15+
import androidx.compose.ui.platform.LocalDensity
16+
import androidx.compose.ui.unit.Dp
17+
import androidx.compose.ui.unit.DpSize
18+
import androidx.compose.ui.unit.IntSize
19+
import androidx.compose.ui.unit.dp
20+
import com.smarttoolfactory.gesture.PointerRequisite
21+
import com.smarttoolfactory.gesture.detectPointerTransformGestures
22+
import kotlin.math.abs
23+
24+
@Composable
25+
fun TransformLayout(
26+
modifier: Modifier = Modifier,
27+
enabled: Boolean = true,
28+
handleRadius: Dp = 15.dp,
29+
handlePlacement: HandlePlacement = HandlePlacement.Corner,
30+
onDown: (Transform) -> Unit = {},
31+
onMove: (Transform) -> Unit = {},
32+
onUp: (Transform) -> Unit = {},
33+
content: @Composable () -> Unit
34+
) {
35+
36+
MorphSubcomposeLayout(
37+
modifier = modifier
38+
.border(3.dp, Color.Green)
39+
.requiredSizeIn(
40+
minWidth = handleRadius * 2,
41+
minHeight = handleRadius * 2
42+
),
43+
handleRadius = handleRadius.coerceAtLeast(12.dp),
44+
updatePhysicalSize = false,
45+
mainContent = {
46+
Box(
47+
modifier = modifier,
48+
contentAlignment = Alignment.Center
49+
) {
50+
content()
51+
}
52+
},
53+
dependentContent = { intSize: IntSize ->
54+
55+
val dpSize = with(LocalDensity.current) {
56+
val rawWidth = intSize.width.toDp()
57+
val rawHeight = intSize.height.toDp()
58+
DpSize(rawWidth, rawHeight)
59+
}
60+
61+
println("Dependent size: $intSize")
62+
TransformLayout(
63+
enabled = enabled,
64+
handleRadius = handleRadius,
65+
dpSize = dpSize,
66+
handlePlacement = handlePlacement,
67+
onDown = onDown,
68+
onMove = onMove,
69+
onUp = onUp,
70+
content = content
71+
)
72+
}
73+
)
74+
}
75+
76+
@Composable
77+
private fun TransformLayout(
78+
enabled: Boolean = true,
79+
handleRadius: Dp = 15.dp,
80+
dpSize: DpSize,
81+
transform: Transform = Transform(),
82+
handlePlacement: HandlePlacement,
83+
onDown: (Transform) -> Unit = {},
84+
onMove: (Transform) -> Unit = {},
85+
onUp: (Transform) -> Unit = {},
86+
content: @Composable () -> Unit
87+
) {
88+
89+
val touchRegionRadius: Float
90+
val minDimension: Float
91+
val size: Size
92+
93+
with(LocalDensity.current) {
94+
touchRegionRadius = handleRadius.coerceAtLeast(12.dp).toPx()
95+
minDimension = (touchRegionRadius * if (handlePlacement == HandlePlacement.Corner) 4 else 6)
96+
97+
size = DpSize(
98+
dpSize.width + handleRadius * 2,
99+
dpSize.height + handleRadius * 2
100+
).toSize()
101+
}
102+
103+
var outerTransform by remember {
104+
mutableStateOf(
105+
transform
106+
)
107+
}
108+
109+
var innerTransform by remember {
110+
mutableStateOf(
111+
transform
112+
)
113+
}
114+
115+
116+
var rectDraw by remember {
117+
mutableStateOf(
118+
Rect(
119+
offset = Offset.Zero,
120+
size = size
121+
)
122+
)
123+
}
124+
125+
val editModifier =
126+
Modifier
127+
.graphicsLayer {
128+
translationX = outerTransform.translationX
129+
translationY = outerTransform.translationY
130+
scaleX = outerTransform.scaleX
131+
scaleY = outerTransform.scaleY
132+
rotationZ = outerTransform.rotation
133+
}
134+
135+
.transform(
136+
enabled = enabled,
137+
size = size,
138+
touchRegionRadius = touchRegionRadius,
139+
minDimension = minDimension,
140+
handlePlacement = handlePlacement,
141+
transform = outerTransform,
142+
onDown = { transformChange: Transform, rect: Rect ->
143+
outerTransform = transformChange
144+
rectDraw = rect
145+
onDown(outerTransform)
146+
},
147+
onMove = { transformChange: Transform, rect: Rect ->
148+
outerTransform = transformChange
149+
rectDraw = rect
150+
onMove(outerTransform)
151+
},
152+
onUp = { transformChange: Transform, rect: Rect ->
153+
outerTransform = transformChange
154+
rectDraw = rect
155+
onUp(outerTransform)
156+
}
157+
)
158+
// Padding is required to keep touch position of handles inside composable
159+
// without padding only 1 quarter of corner handles are in composable
160+
// padding is scaled because scale change also changes padding dimensions
161+
.padding(
162+
horizontal = handleRadius / abs(outerTransform.scaleX),
163+
vertical = handleRadius / abs(outerTransform.scaleY)
164+
)
165+
.clipToBounds()
166+
.graphicsLayer {
167+
translationX = innerTransform.translationX
168+
translationY = innerTransform.translationY
169+
scaleX = innerTransform.scaleX
170+
scaleY = innerTransform.scaleY
171+
}
172+
.pointerInput(Unit) {
173+
detectPointerTransformGestures(
174+
requisite = PointerRequisite.GreaterThan,
175+
numberOfPointers = 1,
176+
onGesture = { _,
177+
gesturePan: Offset,
178+
gestureZoom: Float,
179+
_,
180+
_,
181+
_ ->
182+
183+
val oldZoom = innerTransform.scaleX
184+
val offset =
185+
Offset(innerTransform.translationX, innerTransform.translationY)
186+
val zoom = (oldZoom * gestureZoom).coerceIn(1f, 3f)
187+
val newOffset = offset + gesturePan
188+
189+
val maxX = (size.width * (zoom - 1) / 2f)
190+
val maxY = (size.height * (zoom - 1) / 2f)
191+
192+
innerTransform = innerTransform.copy(
193+
translationX = newOffset.x.coerceIn(-maxX, maxX),
194+
translationY = newOffset.y.coerceIn(-maxY, maxY),
195+
scaleX = zoom,
196+
scaleY = zoom
197+
)
198+
}
199+
)
200+
}
201+
202+
TransformImpl(
203+
modifier = editModifier,
204+
enabled = enabled,
205+
dpSize = dpSize,
206+
handleRadius = handleRadius,
207+
touchRegionRadius = touchRegionRadius,
208+
rectDraw = rectDraw,
209+
handlePlacement = handlePlacement,
210+
content = content
211+
)
212+
}
213+
214+
@Composable
215+
private fun TransformImpl(
216+
modifier: Modifier,
217+
enabled: Boolean,
218+
dpSize: DpSize,
219+
handleRadius: Dp,
220+
touchRegionRadius: Float,
221+
rectDraw: Rect,
222+
handlePlacement: HandlePlacement,
223+
content: @Composable () -> Unit
224+
) {
225+
Box(
226+
Modifier.size(
227+
width = dpSize.width + handleRadius * 2,
228+
height = dpSize.height + handleRadius * 2
229+
),
230+
contentAlignment = Alignment.Center
231+
) {
232+
233+
Box(
234+
modifier = modifier.fillMaxSize(),
235+
contentAlignment = Alignment.Center
236+
) {
237+
content()
238+
}
239+
240+
if (enabled) {
241+
HandleOverlay(
242+
modifier = Modifier.fillMaxSize(),
243+
radius = touchRegionRadius,
244+
handlePlacement = handlePlacement,
245+
rectDraw = rectDraw
246+
)
247+
}
248+
}
249+
}
250+

0 commit comments

Comments
 (0)