Make PagerPageHolder a subclass of ReaderPageImageView

This commit is contained in:
Jays2Kings 2022-05-10 22:26:58 -04:00
parent df168e7154
commit 8dd2d2d0d4
2 changed files with 130 additions and 221 deletions

View file

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

View file

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