Skip to content

Commit 0ea1d26

Browse files
author
Your Name
committed
Add support for menu-based zooming on non-touchscreens
1 parent aa7c665 commit 0ea1d26

File tree

5 files changed

+158
-5
lines changed

5 files changed

+158
-5
lines changed

app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java

+14-5
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import app.grapheneos.pdfviewer.databinding.PdfviewerBinding;
5151
import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
5252
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
53+
import app.grapheneos.pdfviewer.fragment.SetZoomFragment;
5354
import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment;
5455
import app.grapheneos.pdfviewer.ktx.ViewKt;
5556
import app.grapheneos.pdfviewer.loader.DocumentPropertiesAsyncTaskLoader;
@@ -443,12 +444,12 @@ public boolean onTapUp() {
443444

444445
@Override
445446
public void onZoom(float scaleFactor, float focusX, float focusY) {
446-
zoom(scaleFactor, focusX, focusY, false);
447+
onZoomPage(scaleFactor, focusX, focusY, false);
447448
}
448449

449450
@Override
450451
public void onZoomEnd() {
451-
zoomEnd();
452+
onZoomPageEnd();
452453
}
453454
});
454455

@@ -643,15 +644,15 @@ private void shareDocument() {
643644
}
644645
}
645646

646-
private void zoom(float scaleFactor, float focusX, float focusY, boolean end) {
647+
public void onZoomPage(float scaleFactor, float focusX, float focusY, boolean end) {
647648
mZoomRatio = Math.min(Math.max(mZoomRatio * scaleFactor, MIN_ZOOM_RATIO), MAX_ZOOM_RATIO);
648649
mZoomFocusX = focusX;
649650
mZoomFocusY = focusY;
650651
renderPage(end ? 1 : 2);
651652
invalidateOptionsMenu();
652653
}
653654

654-
private void zoomEnd() {
655+
public void onZoomPageEnd() {
655656
renderPage(1);
656657
}
657658

@@ -721,7 +722,7 @@ public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
721722
R.id.action_next, R.id.action_previous, R.id.action_first, R.id.action_last,
722723
R.id.action_rotate_clockwise, R.id.action_rotate_counterclockwise,
723724
R.id.action_view_document_properties, R.id.action_share, R.id.action_save_as,
724-
R.id.action_outline));
725+
R.id.action_outline, R.id.action_set_zoom));
725726
if (BuildConfig.DEBUG) {
726727
ids.add(R.id.debug_action_toggle_text_layer_visibility);
727728
ids.add(R.id.debug_action_crash_webview);
@@ -750,6 +751,7 @@ public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
750751
enableDisableMenuItem(menu.findItem(R.id.action_next), mPage < mNumPages);
751752
enableDisableMenuItem(menu.findItem(R.id.action_previous), mPage > 1);
752753
enableDisableMenuItem(menu.findItem(R.id.action_save_as), mUri != null);
754+
enableDisableMenuItem(menu.findItem(R.id.action_set_zoom), mUri != null);
753755
enableDisableMenuItem(menu.findItem(R.id.action_view_document_properties),
754756
mDocumentProperties != null);
755757

@@ -807,6 +809,13 @@ public boolean onOptionsItemSelected(MenuItem item) {
807809
new JumpToPageFragment()
808810
.show(getSupportFragmentManager(), JumpToPageFragment.TAG);
809811
return true;
812+
} else if (itemId == R.id.action_set_zoom) {
813+
SetZoomFragment zoomFragment = new SetZoomFragment(mZoomRatio, MIN_ZOOM_RATIO, MAX_ZOOM_RATIO);
814+
// TODO: horizontally center the zooming focus.
815+
// Need to get the coordinates of viewport top-center.
816+
// zoomFragment.setZoomFocusX((float) binding.webview.getWidth() / 2);
817+
zoomFragment.show(getSupportFragmentManager(), SetZoomFragment.TAG);
818+
return true;
810819
} else if (itemId == R.id.action_share) {
811820
shareDocument();
812821
return true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package app.grapheneos.pdfviewer.fragment
2+
3+
import android.app.Dialog
4+
import android.content.DialogInterface
5+
import android.os.Bundle
6+
import android.view.Gravity
7+
import android.widget.FrameLayout
8+
import android.widget.LinearLayout
9+
import android.widget.SeekBar
10+
import android.widget.TextView
11+
import androidx.core.view.marginTop
12+
import androidx.fragment.app.DialogFragment
13+
import app.grapheneos.pdfviewer.PdfViewer
14+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
15+
import kotlin.math.ln
16+
import kotlin.math.pow
17+
18+
class SetZoomFragment(
19+
private var mCurrentViewerZoomRatio: Double,
20+
private var mMinZoomRatio: Double,
21+
private var mMaxZoomRatio: Double,
22+
) : DialogFragment() {
23+
24+
companion object {
25+
const val TAG = "SetZoomFragment"
26+
private const val STATE_SEEKBAR_CUR = "seekbar_cur"
27+
private const val STATE_SEEKBAR_MIN = "seekbar_min"
28+
private const val STATE_SEEKBAR_MAX = "seekbar_max"
29+
private const val STATE_VIEWER_CUR = "viewer_cur"
30+
private const val STATE_VIEWER_MIN = "viewer_min"
31+
private const val STATE_VIEWER_MAX = "viewer_max"
32+
private const val STATE_ZOOM_FOCUSX = "viewer_zoom_focusx"
33+
private const val STATE_ZOOM_FOCUSY = "viewer_zoom_focusy"
34+
private const val SEEKBAR_RESOLUTION = 1024
35+
}
36+
37+
private val mSeekBar: SeekBar by lazy { SeekBar(requireActivity()) }
38+
private val mZoomLevelText: TextView by lazy { TextView(requireActivity()) }
39+
40+
private var mZoomFocusX: Float = 0.0f
41+
public fun setZoomFocusX(value: Float) {mZoomFocusX = value}
42+
private var mZoomFocusY: Float = 0.0f
43+
public fun setZoomFocusY(value: Float) {mZoomFocusY = value}
44+
45+
private fun progressToZoom(progress: Int): Double {
46+
val progressClip = progress.coerceAtLeast(0).coerceAtMost(SEEKBAR_RESOLUTION);
47+
return mMinZoomRatio * (mMaxZoomRatio / mMinZoomRatio).pow(progressClip.toDouble() / SEEKBAR_RESOLUTION)
48+
}
49+
50+
private fun zoomToProgress(zoom: Double): Int {
51+
val zoomClip = zoom.coerceAtLeast(mMinZoomRatio).coerceAtMost(mMaxZoomRatio);
52+
return (SEEKBAR_RESOLUTION * ln(zoomClip / mMinZoomRatio) / ln(mMaxZoomRatio / mMinZoomRatio)).toInt()
53+
}
54+
55+
fun refreshZoomText(progress: Int) {
56+
mZoomLevelText.text = "${(progressToZoom(progress) * 100).toInt()}%"
57+
}
58+
59+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
60+
61+
val viewerActivity: PdfViewer = (requireActivity() as PdfViewer)
62+
63+
if (savedInstanceState != null) {
64+
val progress = savedInstanceState.getInt(STATE_SEEKBAR_CUR)
65+
mSeekBar.setMin(savedInstanceState.getInt(STATE_SEEKBAR_MIN))
66+
mSeekBar.setMax(savedInstanceState.getInt(STATE_SEEKBAR_MAX))
67+
mSeekBar.progress = progress
68+
refreshZoomText(progress)
69+
mCurrentViewerZoomRatio = savedInstanceState.getDouble(STATE_VIEWER_CUR)
70+
mMinZoomRatio = savedInstanceState.getDouble(STATE_VIEWER_MIN)
71+
mMaxZoomRatio = savedInstanceState.getDouble(STATE_VIEWER_MAX)
72+
mZoomFocusX = savedInstanceState.getFloat(STATE_ZOOM_FOCUSX)
73+
mZoomFocusY = savedInstanceState.getFloat(STATE_ZOOM_FOCUSY)
74+
} else {
75+
mSeekBar.setMin(0)
76+
mSeekBar.setMax(SEEKBAR_RESOLUTION)
77+
val progress = zoomToProgress(mCurrentViewerZoomRatio)
78+
mSeekBar.setProgress(progress)
79+
refreshZoomText(progress)
80+
}
81+
mSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
82+
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
83+
refreshZoomText(progress)
84+
}
85+
86+
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
87+
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
88+
})
89+
val layout = LinearLayout(requireActivity())
90+
layout.orientation = LinearLayout.VERTICAL
91+
layout.gravity = Gravity.CENTER
92+
val textParams = LinearLayout.LayoutParams(
93+
LinearLayout.LayoutParams.WRAP_CONTENT,
94+
LinearLayout.LayoutParams.WRAP_CONTENT,
95+
)
96+
textParams.setMargins(0, 24, 0, 0) // Margin above the text
97+
layout.addView(mZoomLevelText, textParams)
98+
layout.addView(
99+
mSeekBar, LinearLayout.LayoutParams(
100+
LinearLayout.LayoutParams.MATCH_PARENT,
101+
LinearLayout.LayoutParams.WRAP_CONTENT,
102+
)
103+
)
104+
return MaterialAlertDialogBuilder(requireActivity())
105+
.setView(layout)
106+
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
107+
mSeekBar.clearFocus()
108+
val zoom = progressToZoom(mSeekBar.progress)
109+
viewerActivity.onZoomPage((zoom / mCurrentViewerZoomRatio).toFloat(), mZoomFocusX, mZoomFocusY, true)
110+
}
111+
.setNegativeButton(android.R.string.cancel, null)
112+
.create()
113+
}
114+
115+
override fun onSaveInstanceState(outState: Bundle) {
116+
outState.putInt(STATE_SEEKBAR_CUR, mSeekBar.progress)
117+
outState.putInt(STATE_SEEKBAR_MIN, mSeekBar.min)
118+
outState.putInt(STATE_SEEKBAR_MAX, mSeekBar.max)
119+
outState.putDouble(STATE_VIEWER_CUR, mCurrentViewerZoomRatio)
120+
outState.putDouble(STATE_VIEWER_MIN, mMinZoomRatio)
121+
outState.putDouble(STATE_VIEWER_MAX, mMaxZoomRatio)
122+
outState.putFloat(STATE_ZOOM_FOCUSX, mZoomFocusX)
123+
outState.putFloat(STATE_ZOOM_FOCUSY, mZoomFocusY)
124+
}
125+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="?attr/colorControlNormal"
8+
android:pathData="M12.43 9.18h-1.438V7.754a0.83 0.83 0 0 0 -0.832 -0.828 0.826 0.826 0 0 0 -0.816 0.828V9.18H7.906a0.844 0.844 0 0 0 -0.828 0.828c0.012 0.46 0.383 0.832 0.828 0.832h1.438v1.426c0 0.457 0.37 0.828 0.816 0.828a0.83 0.83 0 0 0 0.832 -0.828V10.84h1.438a0.827 0.827 0 0 0 0.816 -0.832 0.826 0.826 0 0 0 -0.816 -0.828m0 0" />
9+
<path
10+
android:fillColor="?attr/colorControlNormal"
11+
android:pathData="M19.773 17.059l-3.68 -3.692c-0.046 -0.05 -0.109 -0.097 -0.171 -0.148a6.4 6.4 0 0 0 0.828 -3.172c0 -3.617 -2.945 -6.555 -6.566 -6.555a6.56 6.56 0 0 0 -6.555 6.555c0 3.617 2.937 6.566 6.555 6.566a6.5 6.5 0 0 0 2.828 -0.644c0.058 0.101 0.133 0.187 0.222 0.273l3.68 3.68c0.793 0.781 2.082 0.781 2.86 0a2.02 2.02 0 0 0 0 -2.863m-9.59 -2.973a4.037 4.037 0 0 1 -4.027 -4.04 4.034 4.034 0 0 1 4.028 -4.026c2.23 0 4.039 1.808 4.039 4.027a4.04 4.04 0 0 1 -4.04 4.039m0 0" />
12+
</vector>

app/src/main/res/menu/pdf_viewer.xml

+6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@
4343
android:title="@string/action_jump_to_page"
4444
app:showAsAction="ifRoom" />
4545

46+
<item
47+
android:id="@+id/action_set_zoom"
48+
android:icon="@drawable/ic_zoom_in_24dp"
49+
android:title="@string/action_set_zoom"
50+
app:showAsAction="ifRoom" />
51+
4652
<item
4753
android:id="@+id/action_rotate_clockwise"
4854
android:icon="@drawable/ic_rotate_right_24dp"

app/src/main/res/values/strings.xml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<string name="action_first">First page</string>
99
<string name="action_last">Last page</string>
1010
<string name="action_jump_to_page">Jump to page</string>
11+
<string name="action_set_zoom">Zoom</string>
1112
<string name="action_rotate_clockwise">Rotate clockwise</string>
1213
<string name="action_rotate_counterclockwise">Rotate counterclockwise</string>
1314
<string name="action_share">Share</string>

0 commit comments

Comments
 (0)