Skip to content

Commit 6b0ea5c

Browse files
committed
Implement viewport calculation
1 parent 1f21d7b commit 6b0ea5c

7 files changed

Lines changed: 129 additions & 44 deletions

File tree

app/src/main/kotlin/io/bashpsk/emptylibs/TileImageViewScreen.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,7 @@ fun TileImageViewScreen() {
6464
TileImageView(
6565
modifier = Modifier.fillMaxWidth(),
6666
imageBitmap = largeImage,
67-
colorFilter = ImageFilterType.Original.colorFilter,
68-
zoomScale = transformableState.zoom,
69-
centerPosition = layoutPosition,
70-
viewportSize = transformableState.boundSize
67+
colorFilter = ImageFilterType.Original.colorFilter
7168
)
7269
}
7370
}

gesture-ui/src/main/kotlin/io/bashpsk/emptylibs/gestureui/transform/TransformImageExt.kt renamed to gesture-ui/src/main/kotlin/io/bashpsk/emptylibs/gestureui/transform/TransformableGestureExt.kt

File renamed without changes.

image-view/src/main/kotlin/io/bashpsk/emptylibs/imageview/tile/TileImageView.kt

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ import androidx.compose.ui.draw.drawBehind
1313
import androidx.compose.ui.geometry.Offset
1414
import androidx.compose.ui.geometry.Rect
1515
import androidx.compose.ui.geometry.Size
16-
import androidx.compose.ui.geometry.isSpecified
1716
import androidx.compose.ui.graphics.ColorFilter
1817
import androidx.compose.ui.graphics.ImageBitmap
1918
import androidx.compose.ui.layout.ContentScale
2019
import androidx.compose.ui.layout.Layout
21-
import androidx.compose.ui.unit.IntOffset
20+
import androidx.compose.ui.layout.boundsInRoot
21+
import androidx.compose.ui.layout.findRootCoordinates
22+
import androidx.compose.ui.layout.onGloballyPositioned
2223
import androidx.compose.ui.unit.IntSize
2324
import androidx.compose.ui.unit.round
2425
import androidx.compose.ui.unit.roundToIntSize
26+
import androidx.compose.ui.unit.toSize
2527
import io.bashpsk.emptylibs.imageutils.extension.findAspectRatio
2628
import io.bashpsk.emptylibs.imageutils.extension.toSize
2729
import kotlinx.coroutines.launch
@@ -33,14 +35,12 @@ import kotlin.math.roundToInt
3335
*
3436
* @param modifier The modifier to be applied to the layout.
3537
* @param imageBitmap The large [ImageBitmap] to be displayed.
36-
* @param contentScale Strategy used to determine how to scale the image content within the layout bounds.
38+
* @param contentScale Strategy used to determine how to scale the image content within the layout
39+
* bounds.
3740
* @param alignment Alignment parameter used to place the image content in the layout bounds.
3841
* @param alpha Opacity to be applied to the image.
3942
* @param colorFilter ColorFilter to be applied to the image.
4043
* @param tileSize The size of each tile in pixels. Defaults to 512.
41-
* @param zoomScale The current zoom level of the image.
42-
* @param centerPosition The center position of the viewport within the image.
43-
* @param viewportSize The size of the visible area of the image.
4444
*/
4545
@Composable
4646
fun TileImageView(
@@ -50,10 +50,7 @@ fun TileImageView(
5050
alignment: Alignment = Alignment.Center,
5151
alpha: Float = 1.0F,
5252
colorFilter: ColorFilter? = null,
53-
tileSize: Int = 512,
54-
zoomScale: Float = 1.0F,
55-
centerPosition: IntOffset = IntOffset.Zero,
56-
viewportSize: Size = Size.Unspecified
53+
tileSize: Int = 512
5754
) {
5855

5956
val coroutineScope = rememberCoroutineScope()
@@ -73,8 +70,31 @@ fun TileImageView(
7370
Layout(
7471
modifier = modifier
7572
.clipToBounds()
73+
.onGloballyPositioned { coordinates ->
74+
75+
val rootCoordinates = coordinates.findRootCoordinates()
76+
val rootRect = Rect(offset = Offset.Zero, size = rootCoordinates.size.toSize())
77+
val visibleInRoot = coordinates.boundsInRoot().intersect(rootRect)
78+
79+
state.viewportRect = when(visibleInRoot.isEmpty) {
80+
81+
true -> Rect.Zero
82+
83+
false -> Rect(
84+
topLeft = coordinates.localPositionOf(
85+
sourceCoordinates = rootCoordinates,
86+
relativeToSource = visibleInRoot.topLeft
87+
),
88+
bottomRight = coordinates.localPositionOf(
89+
sourceCoordinates = rootCoordinates,
90+
relativeToSource = visibleInRoot.bottomRight
91+
)
92+
)
93+
}
94+
}
7695
.drawBehind {
7796

97+
// var visibleTiles = 0
7898
val srcSize = imageBitmap.toSize()
7999

80100
val baseScale = contentScale.computeScaleFactor(
@@ -91,17 +111,6 @@ fun TileImageView(
91111
layoutDirection = layoutDirection
92112
)
93113

94-
val (positionX, positionY) = centerPosition
95-
val boundSize = (if (viewportSize.isSpecified) viewportSize else size) / zoomScale
96-
97-
val viewportRect = Rect(
98-
offset = Offset(
99-
x = (size.width / 2F) - (positionX / zoomScale) - (boundSize.width / 2F),
100-
y = (size.height / 2F) - (positionY / zoomScale) - (boundSize.height / 2F)
101-
),
102-
size = boundSize
103-
)
104-
105114
state.imageGridList.forEach { tileImage ->
106115

107116
val tileImageRect = Rect(
@@ -115,14 +124,21 @@ fun TileImageView(
115124
)
116125
)
117126

118-
if (viewportRect.overlaps(tileImageRect)) drawImage(
119-
image = tileImage.bitmap,
120-
dstOffset = tileImageRect.topLeft.round(),
121-
dstSize = tileImageRect.size.roundToIntSize(),
122-
alpha = alpha,
123-
colorFilter = colorFilter
124-
)
127+
if (state.viewportRect.overlaps(tileImageRect)) {
128+
129+
// visibleTiles++
130+
131+
drawImage(
132+
image = tileImage.bitmap,
133+
dstOffset = tileImageRect.topLeft.round(),
134+
dstSize = tileImageRect.size.roundToIntSize(),
135+
alpha = alpha,
136+
colorFilter = colorFilter
137+
)
138+
}
125139
}
140+
141+
// "VISIBLE TILES: $visibleTiles".setDebug()
126142
},
127143
content = {}
128144
) { _, constraints ->

image-view/src/main/kotlin/io/bashpsk/emptylibs/imageview/tile/TileImageViewState.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.compose.runtime.getValue
77
import androidx.compose.runtime.mutableStateOf
88
import androidx.compose.runtime.retain.retain
99
import androidx.compose.runtime.setValue
10+
import androidx.compose.ui.geometry.Rect
1011
import androidx.compose.ui.graphics.ImageBitmap
1112
import androidx.compose.ui.graphics.asAndroidBitmap
1213
import androidx.compose.ui.graphics.asImageBitmap
@@ -39,6 +40,12 @@ internal class TileImageViewState() {
3940
*/
4041
internal var imageGridList by mutableStateOf(persistentListOf<TileImageData>())
4142

43+
/**
44+
* The visible rectangular area of the viewport in pixels, used to determine which tiles are
45+
* currently visible.
46+
*/
47+
internal var viewportRect by mutableStateOf(Rect.Zero)
48+
4249
/**
4350
* Parses the given [ImageBitmap] into tiles of the specified size and updates [imageGridList].
4451
*

image-view/src/main/kotlin/io/bashpsk/emptylibs/imageview/transform/TransformImageView.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ fun TransformImageView(
9191
) {
9292

9393
ImageView(
94-
modifier = Modifier.fillMaxSize(),
94+
modifier = Modifier.fillMaxWidth(),
9595
state = state,
9696
model = imageModel,
9797
contentScale = contentScale,
@@ -150,7 +150,7 @@ fun TransformImageView(
150150
) {
151151

152152
ImageView(
153-
modifier = Modifier.fillMaxSize(),
153+
modifier = Modifier.fillMaxWidth(),
154154
state = state,
155155
model = imageModel,
156156
contentScale = contentScale,
@@ -243,7 +243,7 @@ fun TransformImageView(
243243
) { page ->
244244

245245
ImageView(
246-
modifier = Modifier.fillMaxSize(),
246+
modifier = Modifier.fillMaxWidth(),
247247
state = state,
248248
model = imageModelList[page],
249249
contentScale = contentScale,
@@ -421,10 +421,7 @@ private fun ImageView(
421421
modifier = Modifier.fillMaxWidth(),
422422
imageBitmap = model,
423423
contentScale = contentScale,
424-
tileSize = tileSize,
425-
zoomScale = state.zoom,
426-
centerPosition = layoutPosition,
427-
viewportSize = state.boundSize
424+
tileSize = tileSize
428425
)
429426
}
430427
}

jetpack-ui/src/main/kotlin/io/bashpsk/emptylibs/jetpackui/layout/ZoomableLayout.kt

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,64 @@
11
package io.bashpsk.emptylibs.jetpackui.layout
22

33
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.retain.retain
45
import androidx.compose.ui.Modifier
6+
import androidx.compose.ui.geometry.Offset
7+
import androidx.compose.ui.geometry.Rect
58
import androidx.compose.ui.layout.Layout
9+
import androidx.compose.ui.layout.boundsInRoot
10+
import androidx.compose.ui.layout.findRootCoordinates
11+
import androidx.compose.ui.layout.onGloballyPositioned
12+
import androidx.compose.ui.unit.toSize
613
import kotlin.math.roundToInt
714

815
/**
916
* A layout that zooms its content.
1017
*
1118
* @param modifier The modifier to be applied to the layout.
1219
* @param zoomScale The scale factor to apply to the content.
13-
* @param content The content to be zoomed.
20+
* @param content The content to be zoomed, with [ZoomableLayoutScope].
1421
*/
1522
@Composable
16-
inline fun ZoomableLayout(
23+
fun ZoomableLayout(
1724
modifier: Modifier = Modifier,
1825
zoomScale: Float = 1.0F,
19-
crossinline content: @Composable () -> Unit
26+
content: @Composable ZoomableLayoutScope.() -> Unit
2027
) {
2128

29+
val scope = retain { ZoomableLayoutScopeImpl() }
30+
2231
Layout(
23-
modifier = modifier,
24-
content = content
32+
modifier = modifier.onGloballyPositioned { coordinates ->
33+
34+
val rootCoordinates = coordinates.findRootCoordinates()
35+
val rootRect = Rect(offset = Offset.Zero, size = rootCoordinates.size.toSize())
36+
val visibleInRoot = coordinates.boundsInRoot().intersect(rootRect)
37+
38+
val localViewport = when (visibleInRoot.isEmpty) {
39+
40+
true -> Rect.Zero
41+
42+
false -> Rect(
43+
topLeft = coordinates.localPositionOf(
44+
sourceCoordinates = rootCoordinates,
45+
relativeToSource = visibleInRoot.topLeft
46+
),
47+
bottomRight = coordinates.localPositionOf(
48+
sourceCoordinates = rootCoordinates,
49+
relativeToSource = visibleInRoot.bottomRight
50+
)
51+
)
52+
}
53+
54+
scope.viewport = Rect(
55+
left = localViewport.left / zoomScale,
56+
top = localViewport.top / zoomScale,
57+
right = localViewport.right / zoomScale,
58+
bottom = localViewport.bottom / zoomScale
59+
)
60+
},
61+
content = { scope.content() }
2562
) { measurables, constraints ->
2663

2764
val placeables = measurables.map { measurable ->
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.bashpsk.emptylibs.jetpackui.layout
2+
3+
import androidx.compose.runtime.Stable
4+
import androidx.compose.runtime.getValue
5+
import androidx.compose.runtime.mutableStateOf
6+
import androidx.compose.runtime.setValue
7+
import androidx.compose.ui.geometry.Rect
8+
9+
/**
10+
* Receiver scope for the content of a zoomable layout.
11+
*
12+
* This scope provides information about the current state of the zoomable area,
13+
* such as the visible portion of the content.
14+
*/
15+
@Stable
16+
interface ZoomableLayoutScope {
17+
18+
val viewport: Rect
19+
}
20+
21+
/**
22+
* Implementation of [ZoomableLayoutScope] that manages the current viewport state.
23+
*
24+
* This class tracks the visible area of the zoomable layout using a reactive [viewport]
25+
* property, allowing UI components to respond to changes in position and scale.
26+
*/
27+
@Stable
28+
internal class ZoomableLayoutScopeImpl : ZoomableLayoutScope {
29+
30+
override var viewport: Rect by mutableStateOf(Rect.Zero)
31+
}

0 commit comments

Comments
 (0)