mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-14 04:35:05 +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:
parent
71ddb16574
commit
d8719ceee9
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user