diff --git a/app/src/main/assets/viewer.js b/app/src/main/assets/viewer.js index 40e06b6..a8b5489 100644 --- a/app/src/main/assets/viewer.js +++ b/app/src/main/assets/viewer.js @@ -7,13 +7,12 @@ let renderPending = false; let renderPendingLazy = false; const canvas = document.getElementById('content'); let orientationDegrees = 0; -let zoomLevel = 100; +let zoomRatio = 1; let textLayerDiv = document.getElementById("text"); -const zoomLevels = [50, 75, 100, 125, 150]; let task = null; let newPageNumber = 0; -let newZoomLevel = 0; +let newZoomRatio = 1; let useRender; const cache = []; @@ -65,13 +64,13 @@ function renderPage(pageNumber, lazy, prerender, prerenderTrigger=0) { useRender = !prerender; newPageNumber = pageNumber; - newZoomLevel = zoomLevels[channel.getZoomLevel()]; + newZoomRatio = channel.getZoomRatio(); orientationDegrees = channel.getDocumentOrientationDegrees(); - console.log("page: " + pageNumber + ", zoom: " + newZoomLevel + + console.log("page: " + pageNumber + ", zoom: " + newZoomRatio + ", orientationDegrees: " + orientationDegrees + ", prerender: " + prerender); for (let i = 0; i < cache.length; i++) { const cached = cache[i]; - if (cached.pageNumber === pageNumber && cached.zoomLevel === newZoomLevel && + if (cached.pageNumber === pageNumber && cached.zoomRatio === newZoomRatio && cache.orientationDegrees === orientationDegrees) { if (useRender) { cache.splice(i, 1); @@ -95,7 +94,7 @@ function renderPage(pageNumber, lazy, prerender, prerenderTrigger=0) { } const newCanvas = document.createElement("canvas"); - const viewport = page.getViewport({scale: newZoomLevel / 100, rotation: orientationDegrees}) + const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees}) const ratio = window.devicePixelRatio; newCanvas.height = viewport.height * ratio; newCanvas.width = viewport.width * ratio; @@ -105,11 +104,11 @@ function renderPage(pageNumber, lazy, prerender, prerenderTrigger=0) { newContext.scale(ratio, ratio); if (useRender) { - if (newZoomLevel !== zoomLevel) { + if (newZoomRatio !== zoomRatio) { canvas.style.height = viewport.height + "px"; canvas.style.width = viewport.width + "px"; } - zoomLevel = newZoomLevel; + zoomRatio = newZoomRatio; } task = page.render({ @@ -155,7 +154,7 @@ function renderPage(pageNumber, lazy, prerender, prerenderTrigger=0) { } cache.push({ pageNumber: pageNumber, - zoomLevel: newZoomLevel, + zoomRatio: newZoomRatio, orientationDegrees: orientationDegrees, canvas: newCanvas, textLayerDiv: newTextLayerDiv @@ -170,7 +169,7 @@ function renderPage(pageNumber, lazy, prerender, prerenderTrigger=0) { function onRenderPage(lazy) { if (pageRendering) { - if (newPageNumber === channel.getPage() && newZoomLevel === zoomLevels[channel.getZoomLevel()] && + if (newPageNumber === channel.getPage() && getZoomRatio === channel.getZoomRatio() && orientationDegrees === channel.getDocumentOrientationDegrees()) { useRender = true; return; diff --git a/app/src/main/java/org/grapheneos/pdfviewer/GestureHelper.java b/app/src/main/java/org/grapheneos/pdfviewer/GestureHelper.java new file mode 100644 index 0000000..d85ddd3 --- /dev/null +++ b/app/src/main/java/org/grapheneos/pdfviewer/GestureHelper.java @@ -0,0 +1,90 @@ +package org.grapheneos.pdfviewer; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.util.DisplayMetrics; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; + +/* + The GestureHelper present a simple gesture api for the PdfViewer +*/ + +class GestureHelper { + public interface GestureListener { + boolean onTapUp(); + // Can be replaced with ratio when supported + void onZoomIn(int steps); + void onZoomOut(int steps); + void onSwipeEdgeLeft(); + void onSwipeEdgeRight(); + } + + private static final int SPAN_STEP = 150; + + @SuppressLint("ClickableViewAccessibility") + static void attach(Activity context, View gestureView, GestureListener listener) { + DisplayMetrics displayMetrics = new DisplayMetrics(); + context.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + + final GestureDetector detector = new GestureDetector(context, + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent motionEvent) { + return listener.onTapUp(); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + float diffX = e2.getX() - e1.getX(); + if (diffX > 0 && e1.getX() < 10.0) { + listener.onSwipeEdgeRight(); + } else if (diffX < 0 && e1.getX() > displayMetrics.widthPixels - 10.0) { + listener.onSwipeEdgeLeft(); + } else { + return false; + } + return true; + } + }); + + final ScaleGestureDetector scaleDetector = new ScaleGestureDetector(context, + new ScaleGestureDetector.SimpleOnScaleGestureListener() { + // As the zoom value is discrete we listen to scaling step and not scaling ratio + float initialSpan; + int prevNbStep; + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + initialSpan = detector.getCurrentSpan(); + prevNbStep = 0; + return super.onScaleBegin(detector); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + float spanDiff = initialSpan - detector.getCurrentSpan(); + int curNbStep = (int) (spanDiff/SPAN_STEP); + if (curNbStep != prevNbStep) { + int stepDiff = curNbStep - prevNbStep; + if (stepDiff > 0) { + listener.onZoomOut(stepDiff); + } else { + listener.onZoomIn(Math.abs(stepDiff)); + } + prevNbStep = curNbStep; + } + return true; + } + }); + + gestureView.setOnTouchListener((view, motionEvent) -> { + detector.onTouchEvent(motionEvent); + scaleDetector.onTouchEvent(motionEvent); + return false; + }); + } + +} diff --git a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewer.java index 6ab0b7c..61a544b 100644 --- a/app/src/main/java/org/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/org/grapheneos/pdfviewer/PdfViewer.java @@ -8,12 +8,10 @@ import android.net.Uri; import android.os.Bundle; import android.util.Log; -import android.view.GestureDetector; import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.MotionEvent; import android.view.View; import android.webkit.CookieManager; import android.webkit.JavascriptInterface; @@ -43,7 +41,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader private static final String STATE_URI = "uri"; private static final String STATE_PAGE = "page"; - private static final String STATE_ZOOM_LEVEL = "zoomLevel"; + private static final String STATE_ZOOM_RATIO = "zoomRatio"; private static final String STATE_DOCUMENT_ORIENTATION_DEGREES = "documentOrientationDegrees"; private static final String KEY_PROPERTIES = "properties"; @@ -76,8 +74,8 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader "usb 'none'; " + "vr 'none'"; - private static final int MIN_ZOOM_LEVEL = 0; - private static final int MAX_ZOOM_LEVEL = 4; + private static final float MIN_ZOOM_RATIO = 0.5f; + private static final float MAX_ZOOM_RATIO = 1.5f; private static final int ALPHA_LOW = 130; private static final int ALPHA_HIGH = 255; private static final int ACTION_OPEN_DOCUMENT_REQUEST_CODE = 1; @@ -88,7 +86,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader private Uri mUri; public int mPage; public int mNumPages; - private int mZoomLevel = 2; + private float mZoomRatio = 1f; private int mDocumentOrientationDegrees; private int mDocumentState; private int windowInsetTop; @@ -111,8 +109,8 @@ public int getPage() { } @JavascriptInterface - public int getZoomLevel() { - return mZoomLevel; + public float getZoomRatio() { + return mZoomRatio; } @JavascriptInterface @@ -234,10 +232,10 @@ public void onPageFinished(WebView view, String url) { showSystemUi(); - final GestureDetector detector = new GestureDetector(PdfViewer.this, - new GestureDetector.SimpleOnGestureListener() { + GestureHelper.attach(PdfViewer.this, mWebView, + new GestureHelper.GestureListener() { @Override - public boolean onSingleTapUp(MotionEvent motionEvent) { + public boolean onTapUp() { if (mUri != null) { mWebView.evaluateJavascript("isTextSelected()", selection -> { if (!Boolean.valueOf(selection)) { @@ -253,11 +251,37 @@ public boolean onSingleTapUp(MotionEvent motionEvent) { } return false; } + + @Override + public void onZoomIn(int steps) { + for (int i = 0; i < steps; i++) { + zoomIn(); + } + } + + @Override + public void onZoomOut(int steps) { + for (int i = 0; i < steps; i++) { + zoomOut(); + } + } + + + @Override + public void onSwipeEdgeRight() { + if (mPage > 0) { + onJumpToPageInDocument(mPage - 1); + } + } + + + @Override + public void onSwipeEdgeLeft() { + if (mPage < mNumPages) { + onJumpToPageInDocument(mPage + 1); + } + } }); - mWebView.setOnTouchListener((view, motionEvent) -> { - detector.onTouchEvent(motionEvent); - return false; - }); mTextView = new TextView(this); mTextView.setBackgroundColor(Color.DKGRAY); @@ -284,7 +308,7 @@ public boolean onSingleTapUp(MotionEvent motionEvent) { if (savedInstanceState != null) { mUri = savedInstanceState.getParcelable(STATE_URI); mPage = savedInstanceState.getInt(STATE_PAGE); - mZoomLevel = savedInstanceState.getInt(STATE_ZOOM_LEVEL); + mZoomRatio = savedInstanceState.getFloat(STATE_ZOOM_RATIO); mDocumentOrientationDegrees = savedInstanceState.getInt(STATE_DOCUMENT_ORIENTATION_DEGREES); } @@ -340,6 +364,22 @@ private void openDocument() { startActivityForResult(intent, ACTION_OPEN_DOCUMENT_REQUEST_CODE); } + private void zoomIn() { + if (mZoomRatio < MAX_ZOOM_RATIO) { + mZoomRatio += 0.25f; + renderPage(true); + invalidateOptionsMenu(); + } + } + + private void zoomOut() { + if (mZoomRatio > MIN_ZOOM_RATIO) { + mZoomRatio -= 0.25f; + renderPage(true); + invalidateOptionsMenu(); + } + } + private static void enableDisableMenuItem(MenuItem item, boolean enable) { if (enable) { if (!item.isEnabled()) { @@ -382,7 +422,7 @@ public void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); savedInstanceState.putParcelable(STATE_URI, mUri); savedInstanceState.putInt(STATE_PAGE, mPage); - savedInstanceState.putInt(STATE_ZOOM_LEVEL, mZoomLevel); + savedInstanceState.putFloat(STATE_ZOOM_RATIO, mZoomRatio); savedInstanceState.putInt(STATE_DOCUMENT_ORIENTATION_DEGREES, mDocumentOrientationDegrees); } @@ -444,18 +484,10 @@ public boolean onPrepareOptionsMenu(Menu menu) { mDocumentState = STATE_END; } - switch (mZoomLevel) { - case MAX_ZOOM_LEVEL: - enableDisableMenuItem(menu.findItem(R.id.action_zoom_in), false); - return true; - case MIN_ZOOM_LEVEL: - enableDisableMenuItem(menu.findItem(R.id.action_zoom_out), false); - return true; - default: - enableDisableMenuItem(menu.findItem(R.id.action_zoom_in), true); - enableDisableMenuItem(menu.findItem(R.id.action_zoom_out), true); - return true; - } + enableDisableMenuItem(menu.findItem(R.id.action_zoom_in), mZoomRatio != MAX_ZOOM_RATIO); + enableDisableMenuItem(menu.findItem(R.id.action_zoom_out), mZoomRatio != MIN_ZOOM_RATIO); + + return true; } @Override @@ -482,19 +514,11 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); case R.id.action_zoom_out: - if (mZoomLevel > 0) { - mZoomLevel--; - renderPage(true); - invalidateOptionsMenu(); - } + zoomOut(); return true; case R.id.action_zoom_in: - if (mZoomLevel < MAX_ZOOM_LEVEL) { - mZoomLevel++; - renderPage(true); - invalidateOptionsMenu(); - } + zoomIn(); return true; case R.id.action_rotate_clockwise: @@ -513,7 +537,7 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.action_jump_to_page: new JumpToPageFragment() - .show(getSupportFragmentManager(), JumpToPageFragment.TAG); + .show(getSupportFragmentManager(), JumpToPageFragment.TAG); return true; default: