Skip to content

Commit 9e9cdc0

Browse files
committed
Fix PDF zoom gesture conflicts with per-page zoom approach
Replace the old zoom implementation that had gesture conflicts: - Remove detectTransformGestures from parent Box - Remove scale/offsetX/offsetY manual state variables - Remove graphicsLayer transformation on LazyColumn - Remove userScrollEnabled = scale <= 1f logic - Add rememberTransformableState for shared scale state - Apply per-page zoom via graphicsLayer on each Image - LazyColumn now handles all vertical scrolling without conflicts
1 parent ba31781 commit 9e9cdc0

1 file changed

Lines changed: 56 additions & 63 deletions

File tree

app/src/main/java/com/yourname/pdftoolkit/ui/screens/PdfViewerScreen.kt

Lines changed: 56 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import androidx.compose.foundation.Canvas
1919
import androidx.compose.foundation.Image
2020
import androidx.compose.foundation.background
2121
import androidx.compose.foundation.clickable
22+
import androidx.compose.foundation.gestures.awaitEachGesture
23+
import androidx.compose.foundation.gestures.awaitFirstDown
2224
import androidx.compose.foundation.gestures.detectDragGestures
23-
import androidx.compose.foundation.gestures.detectTransformGestures
2425
import androidx.compose.foundation.gestures.rememberTransformableState
2526
import androidx.compose.foundation.gestures.transformable
2627
import androidx.compose.foundation.rememberScrollState
@@ -94,14 +95,22 @@ fun PdfViewerScreen(
9495

9596
// Local UI state
9697
var currentPage by remember { mutableIntStateOf(1) }
97-
var scale by remember { mutableFloatStateOf(1f) }
98-
var offsetX by remember { mutableFloatStateOf(0f) }
99-
var offsetY by remember { mutableFloatStateOf(0f) }
10098
var viewportSize by remember { mutableStateOf(IntSize.Zero) }
10199
var showControls by remember { mutableStateOf(true) }
102100
var showPageSelector by remember { mutableStateOf(false) }
103101
var showClearDialog by remember { mutableStateOf(false) }
104102

103+
// Shared zoom state - hoisted above LazyColumn
104+
// Only scale is shared, horizontal pan is per-page
105+
val zoomState = rememberTransformableState { zoomChange, _, _ ->
106+
// Transformable state handles zoom only
107+
// Scale will be applied to each page individually
108+
}
109+
// Track scale separately for UI controls (zoom buttons, reset, etc.)
110+
var scale by remember { mutableFloatStateOf(1f) }
111+
// Per-page horizontal pan - tracks offsetX for each page
112+
var pagePanX by remember { mutableFloatStateOf(0f) }
113+
105114
// Password state
106115
var showPasswordDialog by remember { mutableStateOf(false) }
107116
var isPasswordError by remember { mutableStateOf(false) }
@@ -352,21 +361,15 @@ fun PdfViewerScreen(
352361
// Zoom controls
353362
IconButton(onClick = {
354363
val newScale = (scale * 1.25f).coerceIn(1f, 5f)
355-
if (newScale <= 1f) {
356-
offsetX = 0f
357-
offsetY = 0f
358-
}
359364
scale = newScale
365+
if (newScale <= 1f) pagePanX = 0f
360366
}) {
361367
Icon(Icons.Default.ZoomIn, contentDescription = "Zoom In")
362368
}
363369
IconButton(onClick = {
364370
val newScale = (scale * 0.8f).coerceIn(1f, 5f)
365-
if (newScale <= 1f) {
366-
offsetX = 0f
367-
offsetY = 0f
368-
}
369371
scale = newScale
372+
if (newScale <= 1f) pagePanX = 0f
370373
}) {
371374
Icon(Icons.Default.ZoomOut, contentDescription = "Zoom Out")
372375
}
@@ -415,8 +418,7 @@ fun PdfViewerScreen(
415418
onClick = {
416419
showMenu = false
417420
scale = 1f
418-
offsetX = 0f
419-
offsetY = 0f
421+
pagePanX = 0f
420422
}
421423
)
422424
if (annotations.isNotEmpty()) {
@@ -553,7 +555,7 @@ fun PdfViewerScreen(
553555
.fillMaxSize()
554556
.padding(paddingValues)
555557
.background(MaterialTheme.colorScheme.background)
556-
.pointerInput(toolState, selectedAnnotationTool, scale, offsetX, offsetY, viewportSize) {
558+
.pointerInput(toolState, selectedAnnotationTool, scale, pagePanX, viewportSize) {
557559
// Enable controls toggle and double-tap zoom
558560
// Disable gestures only when actively drawing (Edit + Tool)
559561
val isDrawing = toolState is PdfTool.Edit && selectedAnnotationTool != AnnotationTool.NONE
@@ -566,18 +568,13 @@ fun PdfViewerScreen(
566568
val newScale = if (scale >= 2f) 1f else 2.5f
567569

568570
if (newScale > 1f) {
569-
// Zoom in towards tap point
571+
// Zoom in towards tap point - adjust horizontal pan
570572
val centerX = viewportSize.width / 2f
571-
val centerY = viewportSize.height / 2f
572573
val focusX = tapOffset.x - centerX
573-
val focusY = tapOffset.y - centerY
574-
575-
offsetX = -focusX * (newScale - 1f)
576-
offsetY = -focusY * (newScale - 1f)
574+
pagePanX = -focusX * (newScale - 1f)
577575
} else {
578-
// Zoom out - reset
579-
offsetX = 0f
580-
offsetY = 0f
576+
// Zoom out - reset pan
577+
pagePanX = 0f
581578
}
582579
scale = newScale
583580
}
@@ -610,12 +607,8 @@ fun PdfViewerScreen(
610607
loadPage = { viewModel.loadPage(it) },
611608
scale = scale,
612609
onScaleChange = { scale = it },
613-
offsetX = offsetX,
614-
offsetY = offsetY,
615-
onOffsetChange = { x, y ->
616-
offsetX = x
617-
offsetY = y
618-
},
610+
pagePanX = pagePanX,
611+
onPagePanXChange = { pagePanX = it },
619612
listState = listState,
620613
isEditMode = isEditMode,
621614
selectedTool = selectedAnnotationTool,
@@ -1048,20 +1041,21 @@ private fun InvalidBitmapPlaceholder() {
10481041
}
10491042

10501043
/**
1051-
* PDF Pages Content with smooth zoom and pan.
1044+
* PDF Pages Content with per-page zoom.
10521045
*
1053-
* Uses LazyColumn with beyondBoundsLayout to preload pages outside viewport.
1054-
* This ensures pages are available when panning while zoomed.
1046+
* Each page is individually zoomable using graphicsLayer on the Image itself.
1047+
* A single shared zoom state is hoisted above LazyColumn.
1048+
* Horizontal pan is handled via offsetX on each page graphicsLayer.
1049+
* LazyColumn handles ALL vertical scrolling always - never disabled.
10551050
*/
10561051
@Composable
10571052
private fun PdfPagesContent(
10581053
totalPages: Int,
10591054
loadPage: suspend (Int) -> Bitmap?,
10601055
scale: Float,
10611056
onScaleChange: (Float) -> Unit,
1062-
offsetX: Float,
1063-
offsetY: Float,
1064-
onOffsetChange: (Float, Float) -> Unit,
1057+
pagePanX: Float,
1058+
onPagePanXChange: (Float) -> Unit,
10651059
listState: LazyListState,
10661060
isEditMode: Boolean,
10671061
selectedTool: AnnotationTool,
@@ -1078,7 +1072,14 @@ private fun PdfPagesContent(
10781072
) {
10791073
var containerSize by remember { mutableStateOf(IntSize.Zero) }
10801074

1081-
// Removed transformableState in favor of pointerInput
1075+
// Transformable state for pinch zoom - only tracks scale
1076+
val transformableState = rememberTransformableState { zoomChange, _, _ ->
1077+
val newScale = (scale * zoomChange).coerceIn(1f, 5f)
1078+
onScaleChange(newScale)
1079+
if (newScale <= 1f) {
1080+
onPagePanXChange(0f)
1081+
}
1082+
}
10821083

10831084
Box(
10841085
modifier = Modifier
@@ -1087,39 +1088,20 @@ private fun PdfPagesContent(
10871088
containerSize = it
10881089
onViewportSizeChange(it)
10891090
}
1091+
// Apply transformable for pinch zoom detection when not drawing
10901092
.then(
10911093
if (isEditMode && selectedTool != AnnotationTool.NONE) {
1092-
Modifier // No gesture handling when drawing
1094+
Modifier // No zoom gestures when drawing
10931095
} else {
1094-
Modifier.pointerInput(Unit) {
1095-
detectTransformGestures(panZoomLock = true) { _, panChange, zoomChange, _ ->
1096-
val newScale = (scale * zoomChange).coerceIn(1f, 5f)
1097-
onScaleChange(newScale)
1098-
1099-
if (newScale > 1f) {
1100-
// Allow both horizontal and vertical panning when zoomed
1101-
onOffsetChange(offsetX + panChange.x, offsetY + panChange.y)
1102-
} else {
1103-
onOffsetChange(0f, 0f)
1104-
}
1105-
}
1106-
}
1096+
Modifier.transformable(state = transformableState)
11071097
}
11081098
)
11091099
) {
11101100
LazyColumn(
11111101
state = listState,
1112-
// Enable scroll only if we aren't drawing, AND if we are not zoomed in
1113-
userScrollEnabled = (!isEditMode || selectedTool == AnnotationTool.NONE) && scale <= 1f,
1114-
modifier = Modifier
1115-
.fillMaxSize()
1116-
.graphicsLayer {
1117-
scaleX = scale
1118-
scaleY = scale
1119-
translationX = offsetX
1120-
translationY = offsetY
1121-
transformOrigin = androidx.compose.ui.graphics.TransformOrigin.Center
1122-
},
1102+
// ALWAYS enable vertical scrolling - LazyColumn handles all vertical scroll
1103+
userScrollEnabled = !isEditMode || selectedTool == AnnotationTool.NONE,
1104+
modifier = Modifier.fillMaxSize(),
11231105
horizontalAlignment = Alignment.CenterHorizontally,
11241106
contentPadding = PaddingValues(vertical = 8.dp)
11251107
) {
@@ -1138,6 +1120,8 @@ private fun PdfPagesContent(
11381120
PdfPageWithAnnotations(
11391121
pageIndex = index,
11401122
loadPage = loadPage,
1123+
scale = scale,
1124+
pagePanX = pagePanX,
11411125
isEditMode = isEditMode,
11421126
selectedTool = selectedTool,
11431127
selectedColor = selectedColor,
@@ -1167,6 +1151,8 @@ private fun PdfPagesContent(
11671151
private fun PdfPageWithAnnotations(
11681152
pageIndex: Int,
11691153
loadPage: suspend (Int) -> Bitmap?,
1154+
scale: Float,
1155+
pagePanX: Float,
11701156
isEditMode: Boolean,
11711157
selectedTool: AnnotationTool,
11721158
selectedColor: Color,
@@ -1211,7 +1197,14 @@ private fun PdfPageWithAnnotations(
12111197
bitmap = bitmapSnapshot.asImageBitmap(),
12121198
contentDescription = "Page ${pageIndex + 1}",
12131199
modifier = Modifier
1214-
.fillMaxWidth(),
1200+
.fillMaxWidth()
1201+
// Per-page zoom: apply scale and horizontal pan to each Image
1202+
.graphicsLayer {
1203+
scaleX = scale
1204+
scaleY = scale
1205+
translationX = pagePanX
1206+
transformOrigin = androidx.compose.ui.graphics.TransformOrigin.Center
1207+
},
12151208
contentScale = ContentScale.FillWidth
12161209
)
12171210
} else {

0 commit comments

Comments
 (0)