From 8dd2d2d0d44a9ef3915c70e196d7c6075b229b9f Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Tue, 10 May 2022 22:26:58 -0400 Subject: [PATCH] Make PagerPageHolder a subclass of ReaderPageImageView --- .../ui/reader/viewer/ReaderPageImageView.kt | 72 ++++- .../ui/reader/viewer/pager/PagerPageHolder.kt | 279 +++++------------- 2 files changed, 130 insertions(+), 221 deletions(-) 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 235db17713..8619583dbb 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 @@ -10,6 +10,7 @@ import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.WindowInsets import android.widget.FrameLayout import androidx.annotation.AttrRes import androidx.annotation.CallSuper @@ -24,6 +25,7 @@ import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE import com.github.chrisbanes.photoview.PhotoView +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.animatorDurationScale @@ -46,7 +48,7 @@ open class ReaderPageImageView @JvmOverloads constructor( private val isWebtoon: Boolean = false, ) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) { - private var pageView: View? = null + protected var pageView: View? = null private var config: Config? = null @@ -55,6 +57,8 @@ open class ReaderPageImageView @JvmOverloads constructor( var onScaleChanged: ((newScale: Float) -> Unit)? = null var onViewClicked: (() -> Unit)? = null + open fun onNeedsLandscapeZoom() { } + @CallSuper open fun onImageLoaded() { onImageLoaded?.invoke() @@ -133,16 +137,38 @@ open class ReaderPageImageView @JvmOverloads constructor( addView(pageView, MATCH_PARENT, MATCH_PARENT) } - private fun SubsamplingScaleImageView.setupZoom(config: Config?) { + protected fun SubsamplingScaleImageView.setupZoom(config: Config?) { // 5x zoom maxScale = scale * MAX_ZOOM_SCALE setDoubleTapZoomScale(scale * 2) config ?: return + + var centerV = 0f 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 }) + PagerConfig.ZoomType.Left -> { + setScaleAndCenter(scale, PointF(0f, 0f)) + } + PagerConfig.ZoomType.Right -> { + setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f)) + centerV = sWidth.toFloat() + } + PagerConfig.ZoomType.Center -> { + setScaleAndCenter(scale, center.also { it?.y = 0f }) + centerV = center?.x ?: 0f + } + } + val insetInfo = config.insetInfo ?: return + val topInsets = insetInfo.topCutoutInset + val bottomInsets = insetInfo.bottomCutoutInset + if (insetInfo.cutoutBehavior == PagerConfig.CUTOUT_START_EXTENDED && + topInsets + bottomInsets > 0 && + insetInfo.scaleTypeIsFullFit + ) { + setScaleAndCenter( + scale, + PointF(centerV, (center?.y?.plus(topInsets)?.minus(bottomInsets) ?: 0f)), + ) } } @@ -154,11 +180,32 @@ open class ReaderPageImageView @JvmOverloads constructor( setMinimumScaleType(config.minimumScaleType) setMinimumDpi(1) // Just so that very small image will be fit for initial load setCropBorders(config.cropBorders) + if (config.insetInfo != null) { + val topInsets = config.insetInfo.topCutoutInset + val bottomInsets = config.insetInfo.bottomCutoutInset + setExtendPastCutout( + config.insetInfo.cutoutBehavior == PagerConfig.CUTOUT_START_EXTENDED && + config.insetInfo.scaleTypeIsFullFit && topInsets + bottomInsets > 0, + ) + if ((config.insetInfo.cutoutBehavior != PagerConfig.CUTOUT_IGNORE || !config.insetInfo.scaleTypeIsFullFit) && + android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q && + config.insetInfo.isFullscreen + ) { + val insets: WindowInsets? = config.insetInfo.insets + setExtraSpace( + 0f, + insets?.displayCutout?.boundingRectTop?.height()?.toFloat() ?: 0f, + 0f, + insets?.displayCutout?.boundingRectBottom?.height()?.toFloat() ?: 0f, + ) + } + } setOnImageEventListener( object : SubsamplingScaleImageView.DefaultOnImageEventListener() { override fun onReady() { // 5x zoom setupZoom(config) + this@ReaderPageImageView.onNeedsLandscapeZoom() this@ReaderPageImageView.onImageLoaded() } @@ -262,13 +309,20 @@ 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: PagerConfig.ZoomType = PagerConfig.ZoomType.Center, val landscapeZoom: Boolean = false, + val insetInfo: InsetInfo? = null, ) - enum class ZoomStartPosition { - LEFT, CENTER, RIGHT - } + data class InsetInfo( + val cutoutBehavior: Int, + val topCutoutInset: Float, + val bottomCutoutInset: Float, + val scaleTypeIsFullFit: Boolean, + val isFullscreen: Boolean, + val isSplitScreen: Boolean, + val insets: WindowInsets?, + ) } private const val MAX_ZOOM_SCALE = 5F 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 205f24718b..a4308e43b4 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 @@ -8,37 +8,24 @@ import android.graphics.BitmapFactory import android.graphics.Color import android.graphics.PointF import android.graphics.RectF -import android.graphics.drawable.Animatable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable -import android.view.GestureDetector +import android.os.Build import android.view.Gravity -import android.view.MotionEvent import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.view.WindowInsets -import android.widget.FrameLayout -import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.core.net.toUri import androidx.core.view.isVisible -import coil.imageLoader -import coil.request.CachePolicy -import coil.request.ImageRequest -import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView -import com.github.chrisbanes.photoview.PhotoView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar -import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.Companion.CUTOUT_IGNORE -import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.Companion.CUTOUT_START_EXTENDED import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType -import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ThemeUtil import eu.kanade.tachiyomi.util.system.bottomCutoutInset @@ -63,7 +50,6 @@ import rx.schedulers.Schedulers import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.io.InputStream -import java.nio.ByteBuffer import java.util.concurrent.TimeUnit import kotlin.math.min import kotlin.math.roundToInt @@ -76,7 +62,7 @@ class PagerPageHolder( val viewer: PagerViewer, val page: ReaderPage, private var extraPage: ReaderPage? = null, -) : FrameLayout(viewer.activity), ViewPagerAdapter.PositionableView { +) : ReaderPageImageView(viewer.activity), ViewPagerAdapter.PositionableView { /** * Item that identifies this view. Needed by the adapter to not recreate views. @@ -89,16 +75,6 @@ class PagerPageHolder( */ private val progressBar = createProgressBar() - /** - * Image view that supports subsampling on zoom. - */ - private var subsamplingImageView: SubsamplingScaleImageView? = null - - /** - * Simple image view only used on GIFs. - */ - private var imageView: ImageView? = null - /** * Retry button used to allow retrying. */ @@ -160,6 +136,35 @@ class PagerPageHolder( ) } + override fun onImageLoaded() { + super.onImageLoaded() + (pageView as? SubsamplingScaleImageView)?.apply { + if (this@PagerPageHolder.extraPage == null && + this@PagerPageHolder.page.longPage == null && + sHeight < sWidth + ) { + this@PagerPageHolder.page.longPage = true + } + } + onImageDecoded() + } + + override fun onNeedsLandscapeZoom() { + (pageView as? SubsamplingScaleImageView)?.apply { + if (viewer.heldForwardZoom?.first == page.index) { + landscapeZoom(viewer.heldForwardZoom?.second) + viewer.heldForwardZoom = null + } else if (isVisibleOnScreen()) { + landscapeZoom(true) + } + } + } + + override fun onImageLoadError() { + super.onImageLoadError() + onImageDecodeError() + } + /** * Called when this view is detached from the window. Unsubscribes any active subscription. */ @@ -173,30 +178,21 @@ class PagerPageHolder( unsubscribeReadImageHeader() scope?.cancel() scope = null - subsamplingImageView?.setOnImageEventListener(null) + (pageView as? SubsamplingScaleImageView)?.setOnImageEventListener(null) } fun onPageSelected(forward: Boolean?) { - subsamplingImageView?.apply { + (pageView as? SubsamplingScaleImageView)?.apply { if (isReady) { landscapeZoom(forward) } else { forward ?: return@apply setOnImageEventListener( object : SubsamplingScaleImageView.DefaultOnImageEventListener() { - override fun onImageLoaded() { - if (this@PagerPageHolder.extraPage == null && - this@PagerPageHolder.page.longPage == null && - sHeight < sWidth - ) { - this@PagerPageHolder.page.longPage = true - } - } - override fun onReady() { - setupZoom() + setupZoom(imageConfig) landscapeZoom(forward) - onImageDecoded() + this@PagerPageHolder.onImageLoaded() } override fun onImageLoadError(e: Exception) { @@ -223,7 +219,7 @@ class PagerPageHolder( * @param fn a function that returns the direction to check for */ private fun canPan(fn: (RectF) -> Float): Boolean { - subsamplingImageView?.let { view -> + (pageView as? SubsamplingScaleImageView)?.let { view -> RectF().let { view.getPanRemaining(it) return fn(it) > 0.01f @@ -251,7 +247,7 @@ class PagerPageHolder( * @param fn a function that computes the new center of the image */ private fun pan(fn: (PointF, SubsamplingScaleImageView) -> PointF) { - subsamplingImageView?.let { view -> + (pageView as? SubsamplingScaleImageView)?.let { view -> val target = fn(view.center ?: return, view) view.animateCenter(target)!! .withEasing(SubsamplingScaleImageView.EASE_OUT_QUAD) @@ -290,39 +286,6 @@ class PagerPageHolder( } } - private fun SubsamplingScaleImageView.setupZoom() { - // 5x zoom - maxScale = scale * MAX_ZOOM_SCALE - setDoubleTapZoomScale(scale * 2) - - var centerV = 0f - val config = viewer.config - when (config.imageZoomType) { - ZoomType.Left -> { - setScaleAndCenter(scale, PointF(0f, 0f)) - } - ZoomType.Right -> { - setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f)) - centerV = sWidth.toFloat() - } - ZoomType.Center -> { - setScaleAndCenter(scale, center.also { it?.y = 0f }) - centerV = center?.x ?: 0f - } - } - val topInsets = viewer.activity.window.decorView.rootWindowInsets.topCutoutInset().toFloat() - val bottomInsets = viewer.activity.window.decorView.rootWindowInsets.bottomCutoutInset().toFloat() - if (config.cutoutBehavior == CUTOUT_START_EXTENDED && - topInsets + bottomInsets > 0 && - config.scaleTypeIsFullFit() - ) { - setScaleAndCenter( - scale, - PointF(centerV, (center?.y?.plus(topInsets)?.minus(bottomInsets) ?: 0f)), - ) - } - } - /** * Observes the status of the page and notify the changes. * @@ -521,28 +484,27 @@ class PagerPageHolder( .doOnNext { isAnimated -> if (!isAnimated) { if (viewer.config.readerTheme >= 2) { - val imageView = initSubsamplingImageView() if (page.bg != null && page.bgType == getBGType(viewer.config.readerTheme, context) + item.hashCode() ) { - imageView.setImage(ImageSource.inputStream(openStream!!)) - imageView.background = page.bg + setImage(openStream!!, false, imageConfig) + pageView?.background = page.bg } // if the user switches to automatic when pages are already cached, the bg needs to be loaded else { val bytesArray = openStream!!.readBytes() val bytesStream = bytesArray.inputStream() - imageView.setImage(ImageSource.inputStream(bytesStream)) + setImage(bytesStream, false, imageConfig) bytesStream.close() - launchUI { + scope?.launchUI { try { - imageView.background = setBG(bytesArray) + pageView?.background = setBG(bytesArray) } catch (e: Exception) { Timber.e(e.localizedMessage) - imageView.background = ColorDrawable(Color.WHITE) + pageView?.background = ColorDrawable(Color.WHITE) } finally { - page.bg = imageView.background + page.bg = pageView?.background page.bgType = getBGType( viewer.config.readerTheme, context, @@ -551,14 +513,12 @@ class PagerPageHolder( } } } else { - val imageView = initSubsamplingImageView() - imageView.setImage(ImageSource.inputStream(openStream!!)) + setImage(openStream!!, false, imageConfig) } } else { - val imageView = initImageView() - imageView.setAnimatedImage(openStream!!) + setImage(openStream!!, true, imageConfig) if (viewer.config.readerTheme >= 2 && page.bg != null) { - imageView.background = page.bg + pageView?.background = page.bg } } } @@ -577,6 +537,25 @@ class PagerPageHolder( .subscribe({}, {}) } + private val imageConfig: Config + get() = Config( + zoomDuration = viewer.config.doubleTapAnimDuration, + minimumScaleType = viewer.config.imageScaleType, + cropBorders = viewer.config.imageCropBorders, + zoomStartPosition = viewer.config.imageZoomType, + landscapeZoom = viewer.config.landscapeZoom, + insetInfo = InsetInfo( + cutoutBehavior = viewer.config.cutoutBehavior, + topCutoutInset = viewer.activity.window.decorView.rootWindowInsets.topCutoutInset().toFloat(), + bottomCutoutInset = viewer.activity.window.decorView.rootWindowInsets.bottomCutoutInset().toFloat(), + scaleTypeIsFullFit = viewer.config.scaleTypeIsFullFit(), + isFullscreen = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + viewer.config.isFullscreen && !viewer.activity.isInMultiWindowMode, + isSplitScreen = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && viewer.activity.isInMultiWindowMode, + insets = viewer.activity.window.decorView.rootWindowInsets, + ), + ) + private suspend fun setBG(bytesArray: ByteArray): Drawable { return withContext(Default) { val preferences by injectLazy() @@ -635,99 +614,6 @@ class PagerPageHolder( } } - /** - * Initializes a subsampling scale view. - */ - private fun initSubsamplingImageView(): SubsamplingScaleImageView { - if (subsamplingImageView != null) return subsamplingImageView!! - - val config = viewer.config - - subsamplingImageView = SubsamplingScaleImageView(context).apply { - setMaxTileSize(GLUtil.maxTextureSize) - setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER) - setDoubleTapZoomDuration(config.doubleTapAnimDuration) - setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) - setMinimumScaleType(config.imageScaleType) - setMinimumTileDpi(180) - setMinimumDpi(1) - setCropBorders(config.imageCropBorders) - val topInsets = viewer.activity.window.decorView.rootWindowInsets.topCutoutInset().toFloat() - val bottomInsets = viewer.activity.window.decorView.rootWindowInsets.bottomCutoutInset().toFloat() - setExtendPastCutout(config.cutoutBehavior == CUTOUT_START_EXTENDED && config.scaleTypeIsFullFit() && topInsets + bottomInsets > 0) - if ((config.cutoutBehavior != CUTOUT_IGNORE || !config.scaleTypeIsFullFit()) && - android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q && - config.isFullscreen && - !viewer.activity.isInMultiWindowMode - ) { - val insets: WindowInsets? = viewer.activity.window.decorView.rootWindowInsets - setExtraSpace( - 0f, - insets?.displayCutout?.boundingRectTop?.height()?.toFloat() ?: 0f, - 0f, - insets?.displayCutout?.boundingRectBottom?.height()?.toFloat() ?: 0f, - ) - } - setOnImageEventListener( - object : SubsamplingScaleImageView.DefaultOnImageEventListener() { - - override fun onImageLoaded() { - if (this@PagerPageHolder.extraPage == null && - this@PagerPageHolder.page.longPage == null && - sHeight < sWidth - ) { - this@PagerPageHolder.page.longPage = true - } - } - override fun onReady() { - setupZoom() - if (viewer.heldForwardZoom?.first == page.index) { - landscapeZoom(viewer.heldForwardZoom?.second) - viewer.heldForwardZoom = null - } else if (isVisibleOnScreen()) { - landscapeZoom(true) - } - onImageDecoded() - } - - override fun onImageLoadError(e: Exception) { - onImageDecodeError() - } - }, - ) - } - addView(subsamplingImageView, MATCH_PARENT, MATCH_PARENT) - return subsamplingImageView!! - } - - /** - * Initializes an image view, used for GIFs. - */ - private fun initImageView(): ImageView { - if (imageView != null) return imageView!! - - imageView = PhotoView(context, null).apply { - adjustViewBounds = true - setZoomTransitionDuration(viewer.config.doubleTapAnimDuration) - setScaleLevels(1f, 2f, 3f) - // Force 2 scale levels on double tap - setOnDoubleTapListener( - object : GestureDetector.SimpleOnGestureListener() { - override fun onDoubleTap(e: MotionEvent): Boolean { - if (scale > 1f) { - setScale(1f, e.x, e.y, true) - } else { - setScale(2f, e.x, e.y, true) - } - return true - } - }, - ) - } - addView(imageView, MATCH_PARENT, MATCH_PARENT) - return imageView!! - } - /** * Initializes a button to retry pages. */ @@ -951,35 +837,6 @@ class PagerPageHolder( } } - /** - * Extension method to set a [stream] into this ImageView. - */ - fun ImageView.setAnimatedImage(image: Any) { - val data = when (image) { - is Drawable -> image - is InputStream -> ByteBuffer.wrap(image.readBytes()) - else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}") - } - val request = ImageRequest.Builder(context) - .data(data) - .memoryCachePolicy(CachePolicy.DISABLED) - .diskCachePolicy(CachePolicy.DISABLED) - .target( - onSuccess = { result -> - setImageDrawable(result) - (result as? Animatable)?.start() - isVisible = true - this@PagerPageHolder.onImageDecoded() - }, - onError = { - this@PagerPageHolder.onImageDecodeError() - }, - ) - .crossfade(false) - .build() - context.imageLoader.enqueue(request) - } - companion object { fun getBGType(readerTheme: Int, context: Context): Int { return if (readerTheme == 3) { @@ -988,5 +845,3 @@ class PagerPageHolder( } } } - -private const val MAX_ZOOM_SCALE = 5F