From 0bb7db395120450ac5dbc66430dfa36204793a91 Mon Sep 17 00:00:00 2001 From: Adam Marek Date: Sat, 18 Apr 2026 00:09:56 +0200 Subject: [PATCH 1/2] Tapping on image opens pinch-to-zoom view --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 3 + app/src/main/assets/image-zoom-handler.js | 10 ++ .../apps/Poche/ui/ImageViewActivity.java | 136 ++++++++++++++++++ .../apps/Poche/ui/ReadArticleActivity.java | 20 +++ .../main/res/layout/activity_image_view.xml | 20 +++ 6 files changed, 190 insertions(+) create mode 100644 app/src/main/assets/image-zoom-handler.js create mode 100644 app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java create mode 100644 app/src/main/res/layout/activity_image_view.xml diff --git a/app/build.gradle b/app/build.gradle index b52220caf..474dd1c36 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,4 +74,5 @@ dependencies { implementation 'com.mikepenz:aboutlibraries:7.1.0' implementation 'com.github.di72nn.wallabag-api-wrapper:api-wrapper:v2.0.0-beta.6' implementation 'org.slf4j:slf4j-android:1.7.36' + implementation 'com.github.chrisbanes:PhotoView:2.3.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9ff75dc6e..51db8ca2e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,6 +55,9 @@ android:name="fr.gaulupeau.apps.Poche.ui.ReadArticleActivity" android:hardwareAccelerated="true" android:configChanges="keyboardHidden|orientation|screenSize"/> + diff --git a/app/src/main/assets/image-zoom-handler.js b/app/src/main/assets/image-zoom-handler.js new file mode 100644 index 000000000..8ab53cb2f --- /dev/null +++ b/app/src/main/assets/image-zoom-handler.js @@ -0,0 +1,10 @@ +document.addEventListener("DOMContentLoaded", function() { + document.addEventListener("click", function(e) { + var target = e.target; + if (target.tagName === "IMG" && target.src) { + e.preventDefault(); + e.stopPropagation(); + hostImageController.onImageClicked(target.src); + } + }, true); +}); diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java new file mode 100644 index 000000000..4265fca9f --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java @@ -0,0 +1,136 @@ +package fr.gaulupeau.apps.Poche.ui; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.ProgressBar; + +import androidx.appcompat.app.AppCompatActivity; + +import com.github.chrisbanes.photoview.PhotoView; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.net.URL; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.network.ImageCacheUtils; + +public class ImageViewActivity extends AppCompatActivity { + + public static final String EXTRA_IMAGE_URL = "ImageViewActivity.imageUrl"; + public static final String EXTRA_ARTICLE_ID = "ImageViewActivity.articleId"; + + private static final String TAG = ImageViewActivity.class.getSimpleName(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_image_view); + + PhotoView photoView = findViewById(R.id.photoView); + ProgressBar progressBar = findViewById(R.id.progressBar); + + String imageUrl = getIntent().getStringExtra(EXTRA_IMAGE_URL); + long articleId = getIntent().getLongExtra(EXTRA_ARTICLE_ID, -1); + + if (imageUrl == null || imageUrl.isEmpty()) { + Log.w(TAG, "onCreate() no image URL"); + finish(); + return; + } + + photoView.setOnClickListener(v -> finish()); + + new Thread(() -> { + Bitmap bitmap = loadBitmap(imageUrl, articleId); + runOnUiThread(() -> { + progressBar.setVisibility(View.GONE); + if (bitmap != null) { + photoView.setImageBitmap(bitmap); + } else { + Log.w(TAG, "onCreate() failed to load image"); + finish(); + } + }); + }).start(); + } + + private Bitmap loadBitmap(String imageUrl, long articleId) { + // Try loading from local cache first + if (articleId >= 0) { + try { + File file = ImageCacheUtils.getCachedImageFile(imageUrl, articleId); + if (file != null) { + Bitmap bitmap = decodeFileScaled(file.getAbsolutePath()); + if (bitmap != null) return bitmap; + } + } catch (Exception e) { + Log.w(TAG, "loadBitmap() cache load failed", e); + } + } + + // Try loading from file:// URL (cached images served to WebView) + if (imageUrl.startsWith("file://")) { + try { + String path = imageUrl.substring("file://".length()); + Bitmap bitmap = decodeFileScaled(path); + if (bitmap != null) return bitmap; + } catch (Exception e) { + Log.w(TAG, "loadBitmap() file URL load failed", e); + } + } + + // Fall back to loading from network + try { + URL url = new URL(imageUrl); + try (InputStream is = url.openStream()) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int n; + while ((n = is.read(chunk)) != -1) { + buffer.write(chunk, 0, n); + } + return decodeBytesScaled(buffer.toByteArray()); + } + } catch (Exception e) { + Log.w(TAG, "loadBitmap() remote load failed", e); + } + + return null; + } + + // Canvas hardware-accelerated draw limit is ~100MB; cap at 4096px (64MB @ ARGB_8888). + private static final int MAX_BITMAP_DIMENSION = 4096; + + private static Bitmap decodeFileScaled(String path) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(path, options); + options.inSampleSize = computeSampleSize(options.outWidth, options.outHeight); + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFile(path, options); + } + + private static Bitmap decodeBytesScaled(byte[] data) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(data, 0, data.length, options); + options.inSampleSize = computeSampleSize(options.outWidth, options.outHeight); + options.inJustDecodeBounds = false; + return BitmapFactory.decodeByteArray(data, 0, data.length, options); + } + + private static int computeSampleSize(int width, int height) { + int sampleSize = 1; + while (width > 0 && height > 0 + && (width / sampleSize > MAX_BITMAP_DIMENSION + || height / sampleSize > MAX_BITMAP_DIMENSION)) { + sampleSize *= 2; + } + return sampleSize; + } +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java index 559e3e145..da592af70 100644 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java @@ -22,6 +22,7 @@ import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; +import android.webkit.JavascriptInterface; import android.webkit.WebViewClient; import android.widget.Button; import android.widget.FrameLayout; @@ -642,6 +643,7 @@ private void initWebView() { initTtsController(); initAnnotationController(); + initImageController(); webViewContent.setWebChromeClient(new WebChromeClient() { private View customView; @@ -800,6 +802,22 @@ private void initTtsController() { webViewContent.addJavascriptInterface(jsTtsController, "hostWebViewTextController"); } + private void initImageController() { + webViewContent.addJavascriptInterface(new Object() { + @SuppressWarnings("unused") + @JavascriptInterface + public void onImageClicked(String imageUrl) { + Log.d(TAG, "onImageClicked() url: " + imageUrl); + Intent intent = new Intent(ReadArticleActivity.this, ImageViewActivity.class); + intent.putExtra(ImageViewActivity.EXTRA_IMAGE_URL, imageUrl); + if (article != null) { + intent.putExtra(ImageViewActivity.EXTRA_ARTICLE_ID, article.getArticleId().longValue()); + } + startActivity(intent); + } + }, "hostImageController"); + } + private void initAnnotationController() { if (!annotationsEnabled) return; @@ -918,6 +936,8 @@ private String getHtmlBase() { private String getExtraHead() { String extra = ""; + extra += "\n\t\t"; + if (annotationsEnabled) { extra += "\n" + "\t\t" + diff --git a/app/src/main/res/layout/activity_image_view.xml b/app/src/main/res/layout/activity_image_view.xml new file mode 100644 index 000000000..fb8135ec2 --- /dev/null +++ b/app/src/main/res/layout/activity_image_view.xml @@ -0,0 +1,20 @@ + + + + + + + + From d3762c099326a3258044a99f76444275c42e7f59 Mon Sep 17 00:00:00 2001 From: Adam Marek Date: Mon, 18 May 2026 22:51:18 +0200 Subject: [PATCH 2/2] PhotoView dependency replaced with ZoomImage --- app/build.gradle | 2 +- .../java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java | 4 ++-- app/src/main/res/layout/activity_image_view.xml | 2 +- app/src/main/res/values-in | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) delete mode 120000 app/src/main/res/values-in diff --git a/app/build.gradle b/app/build.gradle index 474dd1c36..ac1869639 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,5 +74,5 @@ dependencies { implementation 'com.mikepenz:aboutlibraries:7.1.0' implementation 'com.github.di72nn.wallabag-api-wrapper:api-wrapper:v2.0.0-beta.6' implementation 'org.slf4j:slf4j-android:1.7.36' - implementation 'com.github.chrisbanes:PhotoView:2.3.0' + implementation 'io.github.panpf.zoomimage:zoomimage-view:1.4.0' } diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java index 4265fca9f..2fec3287c 100644 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ImageViewActivity.java @@ -9,7 +9,7 @@ import androidx.appcompat.app.AppCompatActivity; -import com.github.chrisbanes.photoview.PhotoView; +import com.github.panpf.zoomimage.ZoomImageView; import java.io.ByteArrayOutputStream; import java.io.File; @@ -31,7 +31,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_image_view); - PhotoView photoView = findViewById(R.id.photoView); + ZoomImageView photoView = findViewById(R.id.photoView); ProgressBar progressBar = findViewById(R.id.progressBar); String imageUrl = getIntent().getStringExtra(EXTRA_IMAGE_URL); diff --git a/app/src/main/res/layout/activity_image_view.xml b/app/src/main/res/layout/activity_image_view.xml index fb8135ec2..6e3bc9395 100644 --- a/app/src/main/res/layout/activity_image_view.xml +++ b/app/src/main/res/layout/activity_image_view.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent" android:background="#000000"> - diff --git a/app/src/main/res/values-in b/app/src/main/res/values-in deleted file mode 120000 index f7118b95e..000000000 --- a/app/src/main/res/values-in +++ /dev/null @@ -1 +0,0 @@ -values-id \ No newline at end of file