@@ -19,8 +19,9 @@ import androidx.compose.foundation.Canvas
1919import androidx.compose.foundation.Image
2020import androidx.compose.foundation.background
2121import androidx.compose.foundation.clickable
22+ import androidx.compose.foundation.gestures.awaitEachGesture
23+ import androidx.compose.foundation.gestures.awaitFirstDown
2224import androidx.compose.foundation.gestures.detectDragGestures
23- import androidx.compose.foundation.gestures.detectTransformGestures
2425import androidx.compose.foundation.gestures.rememberTransformableState
2526import androidx.compose.foundation.gestures.transformable
2627import 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
10571052private 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(
11671151private 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