@@ -37,8 +37,12 @@ public class PdfViewAndroid : IPdfView, IDisposable
3737 private int _currentPage = 0 ;
3838 private int _pageCount = 0 ;
3939 private bool _disposed ;
40+ private bool _wasDetached ;
4041 private float _zoom = 1.0f ;
4142 private bool _zoomNeedsApply ;
43+ private float _offsetX ;
44+ private float _offsetY ;
45+ private bool _positionNeedsApply ;
4246 private readonly HashSet < int > _openedPages = new ( ) ;
4347 private float _lastReportedZoom = 1.0f ;
4448
@@ -50,6 +54,7 @@ public class PdfViewAndroid : IPdfView, IDisposable
5054
5155 private TapListener ? _tapListener ;
5256 private DrawListener ? _drawListener ;
57+ private AttachStateListener ? _attachStateListener ;
5358
5459 public PdfViewAndroid ( Context context )
5560 {
@@ -58,6 +63,12 @@ public PdfViewAndroid(Context context)
5863 // sharing the PdfView's grid cell. Start transparent so an unset MAUI BackgroundColor
5964 // composites the way callers expect.
6065 _pdfView . SetBackgroundColor ( global ::Android . Graphics . Color . Transparent ) ;
66+
67+ // PDFView releases its document when detached from the window - which happens whenever
68+ // another page covers this one, not just on a real teardown - and never reloads on its
69+ // own. Reload on genuine reattach instead of relying on callers to notice and ask for it.
70+ _attachStateListener = new AttachStateListener ( this ) ;
71+ _pdfView . AddOnAttachStateChangeListener ( _attachStateListener ) ;
6172 }
6273
6374 /// <summary>
@@ -175,8 +186,8 @@ private void ReclampZoom()
175186 }
176187
177188 /// <summary>
178- /// Pushes <see cref="_zoom"/> back to the control once it can accept one. Posted so it
179- /// runs after the layout pass that follows a load.
189+ /// Pushes <see cref="_zoom"/> (and, after a reattach, the pan position) back to the
190+ /// control. Posted so it runs after the layout pass that follows a load.
180191 /// </summary>
181192 private void SyncZoom ( )
182193 {
@@ -185,8 +196,11 @@ private void SyncZoom()
185196
186197 _pdfView . Post ( ( ) =>
187198 {
188- if ( ! _disposed && _zoomNeedsApply && TryApplyZoom ( _zoom ) )
199+ if ( ! _disposed && _zoomNeedsApply && TryRestoreZoomAndPosition ( ) )
200+ {
189201 _zoomNeedsApply = false ;
202+ _positionNeedsApply = false ;
203+ }
190204 } ) ;
191205 }
192206
@@ -217,16 +231,43 @@ private bool TryApplyZoom(float zoom)
217231 if ( Math . Abs ( _pdfView . Zoom - zoom ) > float . Epsilon )
218232 {
219233 _pdfView . ZoomCenteredTo ( zoom , new global ::Android . Graphics . PointF ( _pdfView . Width / 2f , _pdfView . Height / 2f ) ) ;
220- _pdfView . LoadPages ( ) ;
221- // Re-settles the page under a snapping display mode, as the animated path does.
222- _pdfView . PerformPageSnap ( ) ;
223- // The viewport now covers a different set of pages.
224- EnsureVisiblePagesOpen ( ) ;
234+ RefreshRenderState ( ) ;
225235 }
226236
227237 return true ;
228238 }
229239
240+ /// <summary>
241+ /// Reapplies zoom and pan after a reload. After a reattach, the saved zoom and offset are
242+ /// a consistent pair, so ZoomTo + MoveTo restores both. Otherwise defers to
243+ /// <see cref="TryApplyZoom"/>, which keeps the offset consistent with the zoom.
244+ /// </summary>
245+ private bool TryRestoreZoomAndPosition ( )
246+ {
247+ if ( ! _positionNeedsApply )
248+ return TryApplyZoom ( _zoom ) ;
249+
250+ if ( _pageCount == 0 || _pdfView . Width <= 0 || _pdfView . Height <= 0 )
251+ return false ;
252+
253+ _pdfView . ZoomTo ( _zoom ) ;
254+ _pdfView . MoveTo ( _offsetX , _offsetY ) ;
255+ RefreshRenderState ( ) ;
256+
257+ return true ;
258+ }
259+
260+ /// <summary>
261+ /// Re-renders after a zoom or move: reloads visible tiles, re-snaps the page, and opens
262+ /// the pages now in view.
263+ /// </summary>
264+ private void RefreshRenderState ( )
265+ {
266+ _pdfView . LoadPages ( ) ;
267+ _pdfView . PerformPageSnap ( ) ;
268+ EnsureVisiblePagesOpen ( ) ;
269+ }
270+
230271 /// <summary>
231272 /// Opens every page a touch inside the current viewport could resolve to.
232273 ///
@@ -543,6 +584,27 @@ public void Reload()
543584
544585 #endregion
545586
587+ private void OnDetachedFromWindow ( )
588+ {
589+ _wasDetached = true ;
590+
591+ // Native state is already reset here, so rely on what ReportZoomIfChanged tracked -
592+ // and only restore position if that tracking was live (not mid-load).
593+ _positionNeedsApply |= ! _zoomNeedsApply && _pageCount > 0 ;
594+ _zoomNeedsApply = true ;
595+ }
596+
597+ // Only reload on a genuine reattach after a real detach - not the initial attach every
598+ // view gets when it's first created, which this would otherwise also fire on.
599+ private void OnReattachedToWindow ( )
600+ {
601+ if ( _disposed || ! _wasDetached )
602+ return ;
603+
604+ _wasDetached = false ;
605+ LoadDocument ( ) ;
606+ }
607+
546608 private void LoadDocument ( bool preserveZoom = true )
547609 {
548610 if ( _source == null )
@@ -555,9 +617,15 @@ private void LoadDocument(bool preserveZoom = true)
555617 // settle on the level to replay afterwards — the same treatment the current page
556618 // gets — and mark it unapplied.
557619 if ( preserveZoom )
620+ {
558621 CaptureZoom ( ) ;
622+ }
559623 else
624+ {
560625 _zoom = Math . Clamp ( 1.0f , _minZoom , _maxZoom ) ;
626+ // New document: don't restore the previous one's position.
627+ _positionNeedsApply = false ;
628+ }
561629
562630 _zoomNeedsApply = true ;
563631
@@ -570,7 +638,7 @@ private void LoadDocument(bool preserveZoom = true)
570638 {
571639 FilePdfSource fileSource => _pdfView . FromFile ( new Java . IO . File ( fileSource . FilePath ) ) ,
572640 UriPdfSource uriSource => _pdfView . FromUri ( global ::Android . Net . Uri . Parse ( uriSource . Uri . ToString ( ) ) ) ,
573- StreamPdfSource streamSource => _pdfView . FromStream ( streamSource . Stream ) ,
641+ StreamPdfSource streamSource => _pdfView . FromStream ( streamSource . OpenDocument ( ) ) ,
574642 BytesPdfSource bytesSource => _pdfView . FromBytes ( bytesSource . Data ) ,
575643 AssetPdfSource assetSource => _pdfView . FromAsset ( assetSource . AssetName ) ,
576644 _ => throw new NotSupportedException ( $ "PDF source type { _source . GetType ( ) . Name } is not supported.")
@@ -670,11 +738,11 @@ private void OnDocumentLoadedWithPageRestore(int pageCount, int pageToRestore)
670738
671739 /// <summary>
672740 /// Publishes the level the control is actually showing, so a caller bound to Zoom sees a
673- /// pinch or double-tap.
741+ /// pinch or double-tap. Also records the pan offset for a reattach restore.
674742 ///
675743 /// This runs on the draw path, so it has to stay cheap: on all but the frames where the
676- /// zoom genuinely moved it is a field read and a float compare, and it allocates only
677- /// when it actually publishes.
744+ /// zoom genuinely moved it is three property reads and a float compare, and it allocates
745+ /// only when it actually publishes.
678746 ///
679747 /// The threshold is what keeps the round trip closed. Publishing sets Zoom on the virtual
680748 /// view, whose handler compares against this same control before pushing anything back,
@@ -688,6 +756,10 @@ private void ReportZoomIfChanged()
688756 if ( _disposed || _zoomNeedsApply || _pageCount == 0 )
689757 return ;
690758
759+ // Before the zoom check - a pan changes the offset without changing zoom.
760+ _offsetX = _pdfView . CurrentXOffset ;
761+ _offsetY = _pdfView . CurrentYOffset ;
762+
691763 var zoom = Math . Clamp ( _pdfView . Zoom , _minZoom , _maxZoom ) ;
692764
693765 if ( Math . Abs ( zoom - _lastReportedZoom ) < ZoomReportThreshold )
@@ -732,17 +804,6 @@ private void OnRendered(int pageCount)
732804 Rendered ? . Invoke ( this , new RenderedEventArgs ( pageCount ) ) ;
733805 }
734806
735- #region Helper Methods
736-
737- private PDFView . Configurator FromStream ( Stream stream )
738- {
739- var memoryStream = new MemoryStream ( ) ;
740- stream . CopyTo ( memoryStream ) ;
741- return _pdfView . FromBytes ( memoryStream . ToArray ( ) ) ;
742- }
743-
744- #endregion
745-
746807 #region Listener Implementations
747808
748809 private class LoadCompleteListener : Java . Lang . Object , IOnLoadCompleteListener
@@ -900,6 +961,32 @@ public void OnInitiallyRendered(int nbPages)
900961 }
901962 }
902963
964+ private class AttachStateListener : Java . Lang . Object , global ::Android . Views . View . IOnAttachStateChangeListener
965+ {
966+ private readonly WeakReference < PdfViewAndroid > _viewRef ;
967+
968+ public AttachStateListener ( PdfViewAndroid view )
969+ {
970+ _viewRef = new WeakReference < PdfViewAndroid > ( view ) ;
971+ }
972+
973+ public void OnViewAttachedToWindow ( global ::Android . Views . View attachedView )
974+ {
975+ if ( _viewRef . TryGetTarget ( out var view ) )
976+ {
977+ view . OnReattachedToWindow ( ) ;
978+ }
979+ }
980+
981+ public void OnViewDetachedFromWindow ( global ::Android . Views . View detachedView )
982+ {
983+ if ( _viewRef . TryGetTarget ( out var view ) )
984+ {
985+ view . OnDetachedFromWindow ( ) ;
986+ }
987+ }
988+ }
989+
903990 #endregion
904991
905992 public void Dispose ( )
@@ -912,6 +999,13 @@ public void Dispose()
912999 // a disposed Java object and throw on the UI thread.
9131000 _disposed = true ;
9141001
1002+ if ( _attachStateListener != null )
1003+ {
1004+ _pdfView ? . RemoveOnAttachStateChangeListener ( _attachStateListener ) ;
1005+ _attachStateListener . Dispose ( ) ;
1006+ _attachStateListener = null ;
1007+ }
1008+
9151009 if ( _tapListener != null )
9161010 {
9171011 _tapListener . Dispose ( ) ;
0 commit comments