diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index 936f70851..2ecf96759 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -29,7 +29,11 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.OnApplyWindowInsetsListener; +import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; @@ -125,6 +129,10 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader private float mZoomRatio = 1f; private float mZoomFocusX = 0f; private float mZoomFocusY = 0f; + private float mInsetLeft = 0f; + private float mInsetTop = 0f; + private float mInsetRight = 0f; + private float mInsetBottom = 0f; private int mDocumentOrientationDegrees; private int mDocumentState; private String mEncryptedDocumentPassword; @@ -138,6 +146,13 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader private PasswordPromptFragment mPasswordPromptFragment; public PdfViewModel viewModel; + private final View.OnLayoutChangeListener appBarOnLayoutChangeListener = + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (binding.toolbar.getVisibility() == View.VISIBLE) { + mInsetTop = bottom - top; + } + }; + private final ActivityResultLauncher openDocumentLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result == null) return; @@ -218,6 +233,26 @@ public float getMaxZoomRatio() { return MAX_ZOOM_RATIO; } + @JavascriptInterface + public float getInsetLeft() { + return mInsetLeft; + } + + @JavascriptInterface + public float getInsetTop() { + return mInsetTop; + } + + @JavascriptInterface + public float getInsetRight() { + return mInsetRight; + } + + @JavascriptInterface + public float getInsetBottom() { + return mInsetBottom; + } + @JavascriptInterface public int getDocumentOrientationDegrees() { return mDocumentOrientationDegrees; @@ -279,7 +314,7 @@ private void showWebViewCrashed() { @SuppressLint({"SetJavaScriptEnabled"}) protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + WindowCompat.enableEdgeToEdge(getWindow()); binding = PdfviewerBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); @@ -308,6 +343,21 @@ protected void onCreate(Bundle savedInstanceState) { // Margins for the toolbar are needed, so that content of the toolbar // is not covered by a system button navigation bar when in landscape. KtUtilsKt.applySystemBarMargins(binding.toolbar, false); + ViewCompat.setOnApplyWindowInsetsListener( + binding.webview, new OnApplyWindowInsetsListener() { + @Override + public @NonNull WindowInsetsCompat onApplyWindowInsets( + @NonNull View v, @NonNull WindowInsetsCompat insets) { + Insets allInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() + | WindowInsetsCompat.Type.displayCutout()); + mInsetLeft = allInsets.left; + mInsetRight = allInsets.right; + // Only set the bottom inset. The top will use the height of the app bar layout + // which includes the status bar/display cutout. + mInsetBottom = allInsets.bottom; + return insets; + } + }); binding.webview.setBackgroundColor(Color.TRANSPARENT); @@ -503,6 +553,8 @@ public void onZoomEnd() { mEncryptedDocumentPassword = savedInstanceState.getString(STATE_ENCRYPTED_DOCUMENT_PASSWORD); } + binding.appBarLayout.addOnLayoutChangeListener(appBarOnLayoutChangeListener); + binding.webviewAlertReload.setOnClickListener(v -> { webViewCrashed = false; recreate(); @@ -529,6 +581,7 @@ private void purgeWebView() { @Override protected void onDestroy() { super.onDestroy(); + binding.appBarLayout.removeOnLayoutChangeListener(appBarOnLayoutChangeListener); purgeWebView(); maybeCloseInputStream(); } diff --git a/app/src/main/res/layout/pdfviewer.xml b/app/src/main/res/layout/pdfviewer.xml index dc73b98af..b1e2bfd3d 100644 --- a/app/src/main/res/layout/pdfviewer.xml +++ b/app/src/main/res/layout/pdfviewer.xml @@ -4,7 +4,13 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + - - document.body.clientHeight; + const isOverflownX = canvas.clientWidth > document.body.clientWidth; + // Translate the text layer to stay aligned with the rendered page including canvas insets and + // grid centering effects. const translate = { - X: Math.max(0, pageWidth - document.body.clientWidth) / 2, - Y: Math.max(0, pageHeight - document.body.clientHeight) / 2 + X: isOverflownX + ? insetLeft - (document.body.clientWidth - pageWidth) / 2 + : (insetLeft - insetRight) / 2, + Y: isOverflownY + ? insetTop - (document.body.clientHeight - pageHeight) / 2 + : (insetTop - insetBottom) / 2 }; layerDiv.style.translate = `${translate.X}px ${translate.Y}px`; } @@ -275,6 +289,13 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { const newContext = newCanvas.getContext("2d", { alpha: false }); newContext.scale(ratio, ratio); + // Add padding to the canvas to allow the page to be scrolled bellow/above any + // system/app ui that might be visible. + canvas.style.paddingLeft = (channel.getInsetLeft() / ratio) + "px"; + canvas.style.paddingTop = (channel.getInsetTop() / ratio) + "px"; + canvas.style.paddingRight = (channel.getInsetRight() / ratio) + "px"; + canvas.style.paddingBottom = (channel.getInsetBottom() / ratio) + "px"; + task = page.render({ canvasContext: newContext, viewport: newViewport