From d8719ceee9d19af73649613695c739c68091f146 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Sun, 13 Feb 2022 00:21:54 +0800 Subject: [PATCH] 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 --- .../data/preference/PreferencesHelper.kt | 4 + .../setting/ReaderReadingModeSettings.kt | 8 ++ .../ui/reader/viewer/ReaderPageImageView.kt | 130 ++++++++++++++++-- .../ui/reader/viewer/pager/PagerConfig.kt | 12 ++ .../ui/reader/viewer/pager/PagerPageHolder.kt | 3 +- .../ui/reader/viewer/pager/PagerViewer.kt | 37 ++++- .../ui/setting/SettingsReaderController.kt | 9 ++ .../tachiyomi/util/view/ViewExtensions.kt | 15 ++ .../main/res/layout/reader_pager_settings.xml | 18 +++ app/src/main/res/values/strings.xml | 2 + 10 files changed, 223 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index d457394682..911ec8e826 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt index 5ef3d9f956..e70e585ff6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderReadingModeSettings.kt +++ b/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()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index cc3d9dc211..c138924bb6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/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) + this@ReaderPageImageView.onImageLoaded() + } + + 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) this@ReaderPageImageView.onImageLoaded() } @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt index fe64dd6162..2fbd8ddfb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt +++ b/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) }) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index e92de7dcf3..2f261a0d42 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index 5820c47b36..f609f5d171 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/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) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt index 6ed3056efe..3976f1d1db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt +++ b/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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 947e5239f8..03e5a9d46d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/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 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) +} diff --git a/app/src/main/res/layout/reader_pager_settings.xml b/app/src/main/res/layout/reader_pager_settings.xml index 3c7631a213..b792de12d8 100644 --- a/app/src/main/res/layout/reader_pager_settings.xml +++ b/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" /> + + + + Previous page Next page + Navigate to pan + Zoom landscape image