Эх сурвалжийг харах

Navigate to pan / landscape zoom (#6481)

* pan if the image is zoomed instead of navigating away
quickly display full landscape image before zooming to fit height in fit to screen

* add Tap to pan preference, defaults to true
add landscape zoom preference, defaults to false

* hide landscape image zoom option if scale is not fit screen

* fix landscape image zoom for first image and loading image

* properly reload pagerholders when landscape zoom option is changed

* enable landscape zoom by default
Gauthier 3 жил өмнө
parent
commit
d8719ceee9

+ 4 - 0
app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt

@@ -129,6 +129,10 @@ class PreferencesHelper(val context: Context) {
 
     fun cropBorders() = flowPrefs.getBoolean("crop_borders", false)
 
+    fun navigateToPan() = flowPrefs.getBoolean("navigate_pan", true)
+
+    fun landscapeZoom() = flowPrefs.getBoolean("landscape_zoom", true)
+
     fun cropBordersWebtoon() = flowPrefs.getBoolean("crop_borders_webtoon", false)
 
     fun webtoonSidePadding() = flowPrefs.getInt("webtoon_side_padding", 0)

+ 8 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt

@@ -74,9 +74,17 @@ class ReaderReadingModeSettings @JvmOverloads constructor(context: Context, attr
         binding.pagerPrefsGroup.tappingInverted.bindToPreference(preferences.pagerNavInverted())
 
         binding.pagerPrefsGroup.pagerNav.bindToPreference(preferences.navigationModePager())
+
+        // Makes so that landscape zoom gets hidden away when image scale type is not fit screen
         binding.pagerPrefsGroup.scaleType.bindToPreference(preferences.imageScaleType(), 1)
+        preferences.imageScaleType()
+            .asImmediateFlow { binding.pagerPrefsGroup.landscapeZoom.isVisible = it == 1 }
+            .launchIn((context as ReaderActivity).lifecycleScope)
+        binding.pagerPrefsGroup.landscapeZoom.bindToPreference(preferences.landscapeZoom())
+
         binding.pagerPrefsGroup.zoomStart.bindToPreference(preferences.zoomStart(), 1)
         binding.pagerPrefsGroup.cropBorders.bindToPreference(preferences.cropBorders())
+        binding.pagerPrefsGroup.navigatePan.bindToPreference(preferences.navigateToPan())
 
         // Makes so that dual page invert gets hidden away when turning of dual page split
         binding.pagerPrefsGroup.dualPageSplit.bindToPreference(preferences.dualPageSplitPaged())

+ 120 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt

@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer
 
 import android.content.Context
 import android.graphics.PointF
+import android.graphics.RectF
 import android.graphics.drawable.Animatable
 import android.graphics.drawable.BitmapDrawable
 import android.graphics.drawable.Drawable
@@ -22,11 +23,14 @@ import coil.request.CachePolicy
 import coil.request.ImageRequest
 import com.davemorrissey.labs.subscaleview.ImageSource
 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
+import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD
+import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_OUT_QUAD
 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
 import com.github.chrisbanes.photoview.PhotoView
 import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
 import eu.kanade.tachiyomi.util.system.GLUtil
 import eu.kanade.tachiyomi.util.system.animatorDurationScale
+import eu.kanade.tachiyomi.util.view.isVisible
 import java.io.InputStream
 import java.nio.ByteBuffer
 
@@ -48,6 +52,8 @@ open class ReaderPageImageView @JvmOverloads constructor(
 
     private var pageView: View? = null
 
+    private var config: Config? = null
+
     var onImageLoaded: (() -> Unit)? = null
     var onImageLoadError: (() -> Unit)? = null
     var onScaleChanged: ((newScale: Float) -> Unit)? = null
@@ -79,7 +85,50 @@ open class ReaderPageImageView @JvmOverloads constructor(
         onViewClicked?.invoke()
     }
 
+    open fun onPageSelected(forward: Boolean) {
+        with(pageView as? SubsamplingScaleImageView) {
+            if (this == null) return
+            if (isReady) {
+                landscapeZoom(forward)
+            } else {
+                setOnImageEventListener(
+                    object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
+                        override fun onReady() {
+                            setupZoom(config)
+                            landscapeZoom(forward)
+                            [email protected]()
+                        }
+
+                        override fun onImageLoadError(e: Exception) {
+                            onImageLoadError()
+                        }
+                    }
+                )
+            }
+        }
+    }
+
+    private fun SubsamplingScaleImageView.landscapeZoom(forward: Boolean) {
+        if (config != null && config!!.landscapeZoom && config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE && sWidth > sHeight && scale == minScale) {
+            handler.postDelayed({
+                val point = when (config!!.zoomStartPosition) {
+                    ZoomStartPosition.LEFT -> if (forward) PointF(0F, 0F) else PointF(sWidth.toFloat(), 0F)
+                    ZoomStartPosition.RIGHT -> if (forward) PointF(sWidth.toFloat(), 0F) else PointF(0F, 0F)
+                    ZoomStartPosition.CENTER -> center.also { it?.y = 0F }
+                }
+
+                val targetScale = height.toFloat() / sHeight.toFloat()
+                animateScaleAndCenter(targetScale, point)!!
+                    .withDuration(500)
+                    .withEasing(EASE_IN_OUT_QUAD)
+                    .withInterruptible(true)
+                    .start()
+            }, 500)
+        }
+    }
+
     fun setImage(drawable: Drawable, config: Config) {
+        this.config = config
         if (drawable is Animatable) {
             prepareAnimatedImageView()
             setAnimatedImage(drawable, config)
@@ -90,6 +139,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
     }
 
     fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) {
+        this.config = config
         if (isAnimated) {
             prepareAnimatedImageView()
             setAnimatedImage(inputStream, config)
@@ -107,6 +157,60 @@ open class ReaderPageImageView @JvmOverloads constructor(
         it.isVisible = false
     }
 
+    /**
+     * Check if the image can be panned to the left
+     */
+    fun canPanLeft(): Boolean = canPan { it.left }
+
+    /**
+     * Check if the image can be panned to the right
+     */
+    fun canPanRight(): Boolean = canPan { it.right }
+
+    /**
+     * Check whether the image can be panned.
+     * @param fn a function that returns the direction to check for
+     */
+    private fun canPan(fn: (RectF) -> Float): Boolean {
+        (pageView as? SubsamplingScaleImageView)?.let { view ->
+            RectF().let {
+                view.getPanRemaining(it)
+                return fn(it) > 0
+            }
+        }
+        return false
+    }
+
+    /**
+     * Pans the image to the left by a screen's width worth.
+     */
+    fun panLeft() {
+        pan { center, view -> center.also { it.x -= view.width / view.scale } }
+    }
+
+    /**
+     * Pans the image to the right by a screen's width worth.
+     */
+    fun panRight() {
+        pan { center, view -> center.also { it.x += view.width / view.scale } }
+    }
+
+    /**
+     * Pans the image.
+     * @param fn a function that computes the new center of the image
+     */
+    private fun pan(fn: (PointF, SubsamplingScaleImageView) -> PointF) {
+        (pageView as? SubsamplingScaleImageView)?.let { view ->
+
+            val target = fn(view.center ?: return, view)
+            view.animateCenter(target)!!
+                .withEasing(EASE_OUT_QUAD)
+                .withDuration(250)
+                .withInterruptible(true)
+                .start()
+        }
+    }
+
     private fun prepareNonAnimatedImageView() {
         if (pageView is SubsamplingScaleImageView) return
         removeView(pageView)
@@ -136,6 +240,18 @@ open class ReaderPageImageView @JvmOverloads constructor(
         addView(pageView, MATCH_PARENT, MATCH_PARENT)
     }
 
+    private fun SubsamplingScaleImageView.setupZoom(config: Config?) {
+        // 5x zoom
+        maxScale = scale * MAX_ZOOM_SCALE
+        setDoubleTapZoomScale(scale * 2)
+
+        when (config?.zoomStartPosition) {
+            ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F))
+            ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F))
+            ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F })
+        }
+    }
+
     private fun setNonAnimatedImage(
         image: Any,
         config: Config
@@ -147,15 +263,8 @@ open class ReaderPageImageView @JvmOverloads constructor(
         setOnImageEventListener(
             object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
                 override fun onReady() {
-                    // 5x zoom
-                    maxScale = scale * MAX_ZOOM_SCALE
-                    setDoubleTapZoomScale(scale * 2)
-
-                    when (config.zoomStartPosition) {
-                        ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F))
-                        ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F))
-                        ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F })
-                    }
+                    setupZoom(config)
+                    if (isVisible()) landscapeZoom(true)
                     [email protected]()
                 }
 
@@ -259,7 +368,8 @@ open class ReaderPageImageView @JvmOverloads constructor(
         val zoomDuration: Int,
         val minimumScaleType: Int = SCALE_TYPE_CENTER_INSIDE,
         val cropBorders: Boolean = false,
-        val zoomStartPosition: ZoomStartPosition = ZoomStartPosition.CENTER
+        val zoomStartPosition: ZoomStartPosition = ZoomStartPosition.CENTER,
+        val landscapeZoom: Boolean = false,
     )
 
     enum class ZoomStartPosition {

+ 12 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt

@@ -41,6 +41,12 @@ class PagerConfig(
     var imageCropBorders = false
         private set
 
+    var navigateToPan = false
+        private set
+
+    var landscapeZoom = false
+        private set
+
     init {
         preferences.readerTheme()
             .register(
@@ -60,6 +66,12 @@ class PagerConfig(
         preferences.cropBorders()
             .register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
 
+        preferences.navigateToPan()
+            .register({ navigateToPan = it })
+
+        preferences.landscapeZoom()
+            .register({ landscapeZoom = it }, { imagePropertyChangedListener?.invoke() })
+
         preferences.navigationModePager()
             .register({ navigationMode = it }, { updateNavigation(navigationMode) })
 

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt

@@ -226,7 +226,8 @@ class PagerPageHolder(
                             zoomDuration = viewer.config.doubleTapAnimDuration,
                             minimumScaleType = viewer.config.imageScaleType,
                             cropBorders = viewer.config.imageCropBorders,
-                            zoomStartPosition = viewer.config.imageZoomType
+                            zoomStartPosition = viewer.config.imageZoomType,
+                            landscapeZoom = viewer.config.landscapeZoom,
                         )
                     )
                     if (!isAnimated) {

+ 33 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt

@@ -6,6 +6,7 @@ import android.view.KeyEvent
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup.LayoutParams
+import androidx.core.view.children
 import androidx.core.view.isGone
 import androidx.core.view.isVisible
 import androidx.viewpager.widget.ViewPager
@@ -154,6 +155,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
         return pager
     }
 
+    /**
+     * Returns the PagerPageHolder for the provided page
+     */
+    private fun getPageHolder(page: ReaderPage): PagerPageHolder? =
+        pager.children
+            .filterIsInstance(PagerPageHolder::class.java)
+            .firstOrNull { it.item.index == page.index }
+
     /**
      * Called when a new page (either a [ReaderPage] or [ChapterTransition]) is marked as active
      */
@@ -161,9 +170,16 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
         val page = adapter.items.getOrNull(position)
         if (page != null && currentPage != page) {
             val allowPreload = checkAllowPreload(page as? ReaderPage)
+            val forward = when {
+                currentPage is ReaderPage && page is ReaderPage ->
+                    page.number > (currentPage as ReaderPage).number
+                currentPage is ChapterTransition.Prev && page is ReaderPage ->
+                    false
+                else -> true
+            }
             currentPage = page
             when (page) {
-                is ReaderPage -> onReaderPageSelected(page, allowPreload)
+                is ReaderPage -> onReaderPageSelected(page, allowPreload, forward)
                 is ChapterTransition -> onTransitionSelected(page)
             }
         }
@@ -192,11 +208,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
      * Called when a [ReaderPage] is marked as active. It notifies the
      * activity of the change and requests the preload of the next chapter if this is the last page.
      */
-    private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean) {
+    private fun onReaderPageSelected(page: ReaderPage, allowPreload: Boolean, forward: Boolean) {
         val pages = page.chapter.pages ?: return
         logcat { "onReaderPageSelected: ${page.number}/${pages.size}" }
         activity.onPageSelected(page)
 
+        // Notify holder of page change
+        getPageHolder(page)?.onPageSelected(forward)
+
         // Skip preload on inserts it causes unwanted page jumping
         if (page is InsertPage) {
             return
@@ -294,7 +313,12 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
      */
     protected open fun moveRight() {
         if (pager.currentItem != adapter.count - 1) {
-            pager.setCurrentItem(pager.currentItem + 1, config.usePageTransitions)
+            val holder = (currentPage as? ReaderPage)?.let { getPageHolder(it) }
+            if (holder != null && config.navigateToPan && holder.canPanRight()) {
+                holder.panRight()
+            } else {
+                pager.setCurrentItem(pager.currentItem + 1, config.usePageTransitions)
+            }
         }
     }
 
@@ -303,7 +327,12 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
      */
     protected open fun moveLeft() {
         if (pager.currentItem != 0) {
-            pager.setCurrentItem(pager.currentItem - 1, config.usePageTransitions)
+            val holder = (currentPage as? ReaderPage)?.let { getPageHolder(it) }
+            if (holder != null && config.navigateToPan && holder.canPanLeft()) {
+                holder.panLeft()
+            } else {
+                pager.setCurrentItem(pager.currentItem - 1, config.usePageTransitions)
+            }
         }
     }
 

+ 9 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt

@@ -183,6 +183,11 @@ class SettingsReaderController : SettingsController() {
                 entryValues = arrayOf("1", "2", "3", "4", "5", "6")
                 summary = "%s"
             }
+            switchPreference {
+                bindTo(preferences.landscapeZoom())
+                titleRes = R.string.pref_landscape_zoom
+                visibleIf(preferences.imageScaleType()) { it == 1 }
+            }
             intListPreference {
                 bindTo(preferences.zoomStart())
                 titleRes = R.string.pref_zoom_start
@@ -199,6 +204,10 @@ class SettingsReaderController : SettingsController() {
                 bindTo(preferences.cropBorders())
                 titleRes = R.string.pref_crop_borders
             }
+            switchPreference {
+                bindTo(preferences.navigateToPan())
+                titleRes = R.string.pref_navigate_pan
+            }
             switchPreference {
                 bindTo(preferences.dualPageSplitPaged())
                 titleRes = R.string.pref_dual_page_split

+ 15 - 0
app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt

@@ -4,7 +4,9 @@ package eu.kanade.tachiyomi.util.view
 
 import android.annotation.SuppressLint
 import android.content.Context
+import android.content.res.Resources
 import android.graphics.Point
+import android.graphics.Rect
 import android.graphics.drawable.Drawable
 import android.text.TextUtils
 import android.view.Gravity
@@ -259,3 +261,16 @@ inline fun <reified T : Drawable> T.copy(context: Context): T? {
         }
     }
 }
+
+fun View?.isVisible(): Boolean {
+    if (this == null) {
+        return false
+    }
+    if (!this.isShown) {
+        return false
+    }
+    val actualPosition = Rect()
+    this.getGlobalVisibleRect(actualPosition)
+    val screen = Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
+    return actualPosition.intersect(screen)
+}

+ 18 - 0
app/src/main/res/layout/reader_pager_settings.xml

@@ -37,6 +37,15 @@
         android:entries="@array/image_scale_type"
         app:title="@string/pref_image_scale_type" />
 
+    <com.google.android.material.switchmaterial.SwitchMaterial
+        android:id="@+id/landscape_zoom"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingStart="16dp"
+        android:paddingEnd="16dp"
+        android:text="@string/pref_landscape_zoom"
+        android:textColor="?android:attr/textColorSecondary" />
+
     <eu.kanade.tachiyomi.widget.MaterialSpinnerView
         android:id="@+id/zoom_start"
         android:layout_width="match_parent"
@@ -53,6 +62,15 @@
         android:text="@string/pref_crop_borders"
         android:textColor="?android:attr/textColorSecondary" />
 
+    <com.google.android.material.switchmaterial.SwitchMaterial
+        android:id="@+id/navigate_pan"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingStart="16dp"
+        android:paddingEnd="16dp"
+        android:text="@string/pref_navigate_pan"
+        android:textColor="?android:attr/textColorSecondary" />
+
     <com.google.android.material.switchmaterial.SwitchMaterial
         android:id="@+id/dual_page_split"
         android:layout_width="match_parent"

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -805,4 +805,6 @@
     <!-- S Pen actions -->
     <string name="spen_previous_page">Previous page</string>
     <string name="spen_next_page">Next page</string>
+    <string name="pref_navigate_pan">Navigate to pan</string>
+    <string name="pref_landscape_zoom">Zoom landscape image</string>
 </resources>