Skip to content

Commit 3a46c1e

Browse files
Merge branch 'feature/image-modifier' into develop
2 parents 84f5275 + cfc4dc5 commit 3a46c1e

File tree

3 files changed

+271
-27
lines changed

3 files changed

+271
-27
lines changed

app/src/main/java/com/smarttoolfactory/composeimage/MainActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ private fun HomeContent() {
9191
1 -> ThumbnailDemo()
9292
2 -> EditScaleDemo()
9393
3 -> EditSizeDemo()
94-
else -> ZoomableImageDemo()
94+
else -> ZoomDemo()
9595
}
9696
}
9797
}
@@ -103,5 +103,5 @@ internal val tabList =
103103
"Image Thumbnail",
104104
"Editable Scale",
105105
"Editable Size",
106-
"Zoomable Image",
106+
"Zoom",
107107
)

app/src/main/java/com/smarttoolfactory/composeimage/demo/ZoomableImageDemo.kt

Lines changed: 152 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ package com.smarttoolfactory.composeimage.demo
22

33
import androidx.compose.foundation.background
44
import androidx.compose.foundation.layout.*
5+
import androidx.compose.foundation.rememberScrollState
6+
import androidx.compose.foundation.verticalScroll
7+
import androidx.compose.material.Slider
58
import androidx.compose.material3.MaterialTheme
69
import androidx.compose.material3.Text
710
import androidx.compose.runtime.*
811
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.draw.shadow
913
import androidx.compose.ui.graphics.Color
1014
import androidx.compose.ui.graphics.ImageBitmap
15+
import androidx.compose.ui.graphics.Path
16+
import androidx.compose.ui.graphics.PathEffect
17+
import androidx.compose.ui.graphics.drawscope.Stroke
1118
import androidx.compose.ui.layout.ContentScale
1219
import androidx.compose.ui.platform.LocalContext
1320
import androidx.compose.ui.res.imageResource
@@ -17,56 +24,176 @@ import androidx.compose.ui.unit.sp
1724
import com.smarttoolfactory.composeimage.ContentScaleSelectionMenu
1825
import com.smarttoolfactory.composeimage.R
1926
import com.smarttoolfactory.image.zoom.ZoomableImage
27+
import com.smarttoolfactory.image.zoom.zoom
28+
import kotlin.math.cos
29+
import kotlin.math.roundToInt
30+
import kotlin.math.sin
2031

2132
@Composable
22-
fun ZoomableImageDemo() {
33+
fun ZoomDemo() {
2334

24-
Column(modifier = Modifier.fillMaxSize()) {
25-
val imageBitmapLarge = ImageBitmap.imageResource(
26-
LocalContext.current.resources,
27-
R.drawable.landscape4
28-
)
35+
Column(
36+
modifier = Modifier
37+
.fillMaxSize()
38+
.verticalScroll(rememberScrollState())
39+
.background(Color(0xffECEFF1))
40+
) {
41+
ZoomableImageDemo()
42+
ZoomModifierDemo()
43+
}
44+
}
2945

30-
var contentScale by remember { mutableStateOf(ContentScale.Fit) }
46+
@Composable
47+
private fun ZoomableImageDemo() {
48+
val imageBitmapLarge = ImageBitmap.imageResource(
49+
LocalContext.current.resources,
50+
R.drawable.landscape4
51+
)
3152

32-
ContentScaleSelectionMenu(contentScale = contentScale) {
33-
contentScale = it
34-
}
53+
var contentScale by remember { mutableStateOf(ContentScale.Fit) }
54+
55+
ContentScaleSelectionMenu(contentScale = contentScale) {
56+
contentScale = it
57+
}
58+
59+
Text(
60+
text = "clipTransformToContentScale false",
61+
fontSize = 16.sp,
62+
fontWeight = FontWeight.Bold,
63+
color = MaterialTheme.colorScheme.primary,
64+
modifier = Modifier.padding(8.dp)
65+
)
66+
ZoomableImage(
67+
modifier = Modifier
68+
.background(Color.LightGray)
69+
.fillMaxWidth()
70+
.aspectRatio(4 / 3f),
71+
imageBitmap = imageBitmapLarge,
72+
contentScale = contentScale,
73+
clipTransformToContentScale = false
74+
)
75+
76+
Spacer(modifier = Modifier.height(20.dp))
3577

78+
Text(
79+
text = "clipTransformToContentScale true",
80+
fontSize = 16.sp,
81+
fontWeight = FontWeight.Bold,
82+
color = MaterialTheme.colorScheme.primary,
83+
modifier = Modifier.padding(8.dp)
84+
)
85+
ZoomableImage(
86+
modifier = Modifier
87+
.background(Color.LightGray)
88+
.fillMaxWidth()
89+
.aspectRatio(4 / 3f),
90+
imageBitmap = imageBitmapLarge,
91+
contentScale = contentScale,
92+
clipTransformToContentScale = true
93+
)
94+
}
95+
96+
@Composable
97+
private fun ZoomModifierDemo() {
98+
Column(
99+
modifier = Modifier
100+
) {
36101
Text(
37-
text = "clipTransformToContentScale false",
102+
text = "Modifier.zoom(clip=true)",
38103
fontSize = 16.sp,
39104
fontWeight = FontWeight.Bold,
40105
color = MaterialTheme.colorScheme.primary,
41106
modifier = Modifier.padding(8.dp)
42107
)
43-
ZoomableImage(
108+
DrawPolygonPath(
44109
modifier = Modifier
45-
.background(Color.LightGray)
110+
.padding(8.dp)
111+
.shadow(1.dp)
112+
.background(Color.White)
46113
.fillMaxWidth()
47-
.aspectRatio(4 / 3f),
48-
imageBitmap = imageBitmapLarge,
49-
contentScale = contentScale,
50-
clipTransformToContentScale = false
114+
.height(200.dp)
115+
.zoom(Unit, clip = true)
51116
)
52117

53118
Spacer(modifier = Modifier.height(20.dp))
54119

55120
Text(
56-
text = "clipTransformToContentScale true",
121+
text = "Modifier.zoom(clip=false)",
57122
fontSize = 16.sp,
58123
fontWeight = FontWeight.Bold,
59124
color = MaterialTheme.colorScheme.primary,
60125
modifier = Modifier.padding(8.dp)
61126
)
62-
ZoomableImage(
127+
DrawPolygonPath(
63128
modifier = Modifier
64-
.background(Color.LightGray)
129+
.padding(8.dp)
130+
.shadow(1.dp, clip = false)
131+
.background(Color.White)
65132
.fillMaxWidth()
66-
.aspectRatio(4 / 3f),
67-
imageBitmap = imageBitmapLarge,
68-
contentScale = contentScale,
69-
clipTransformToContentScale = true
133+
.height(200.dp)
134+
.zoom(Unit, clip = false)
70135
)
71136
}
72-
}
137+
}
138+
139+
140+
@Composable
141+
private fun DrawPolygonPath(modifier: Modifier) {
142+
var sides by remember { mutableStateOf(3f) }
143+
var cornerRadius by remember { mutableStateOf(1f) }
144+
145+
androidx.compose.foundation.Canvas(modifier = modifier) {
146+
val canvasWidth = size.width
147+
val canvasHeight = size.height
148+
val cx = canvasWidth / 2
149+
val cy = canvasHeight / 2
150+
val radius = (canvasHeight - 20.dp.toPx()) / 2
151+
val path = createPolygonPath(cx, cy, sides.roundToInt(), radius)
152+
153+
drawPath(
154+
color = Color.Red,
155+
path = path,
156+
style = Stroke(
157+
width = 4.dp.toPx(),
158+
pathEffect = PathEffect.cornerPathEffect(cornerRadius)
159+
)
160+
)
161+
}
162+
163+
Column(modifier = Modifier.padding(horizontal = 20.dp)) {
164+
androidx.compose.material.Text(text = "Sides ${sides.roundToInt()}")
165+
Slider(
166+
value = sides,
167+
onValueChange = { sides = it },
168+
valueRange = 3f..12f,
169+
steps = 10
170+
)
171+
172+
androidx.compose.material.Text(text = "CornerRadius ${cornerRadius.roundToInt()}")
173+
174+
Slider(
175+
value = cornerRadius,
176+
onValueChange = { cornerRadius = it },
177+
valueRange = 0f..50f,
178+
)
179+
}
180+
}
181+
182+
183+
fun createPolygonPath(cx: Float, cy: Float, sides: Int, radius: Float): Path {
184+
val angle = 2.0 * Math.PI / sides
185+
186+
return Path().apply {
187+
moveTo(
188+
cx + (radius * cos(0.0)).toFloat(),
189+
cy + (radius * sin(0.0)).toFloat()
190+
)
191+
for (i in 1 until sides) {
192+
lineTo(
193+
cx + (radius * cos(angle * i)).toFloat(),
194+
cy + (radius * sin(angle * i)).toFloat()
195+
)
196+
}
197+
close()
198+
}
199+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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

Comments
 (0)