mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -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) | ||||
|   | ||||
| @@ -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()) | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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) }) | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user