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:
Gauthier 2022-02-13 00:21:54 +08:00 committed by GitHub
parent 71ddb16574
commit d8719ceee9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 223 additions and 15 deletions

View file

@ -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)

View file

@ -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())

View file

@ -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 {

View file

@ -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) })

View file

@ -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) {

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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)
}

View file

@ -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"

View file

@ -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>