Răsfoiți Sursa

Reuse reader's image view in MangaFullCoverDialog (#5824)

* MangaFullCoverDialog: Support animated drawable

* Scaled zoom duration

* Wrap reader's image view to be reused in MangaFullCoverDialog

* Cleanups

* Forgot animated stuff for webtoon view

* Cleanups

* Oopsie

* Cleanups

* Consistent max scale for SubsamplingScaleImageView

The max scale will be obtained from the default scale times 3 for
consistent 3x zoom scale.
Ivan Iskandar 3 ani în urmă
părinte
comite
746d35b52b

+ 11 - 15
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFullCoverDialog.kt

@@ -1,7 +1,6 @@
 package eu.kanade.tachiyomi.ui.manga.info
 
 import android.app.Dialog
-import android.graphics.drawable.BitmapDrawable
 import android.graphics.drawable.ColorDrawable
 import android.os.Bundle
 import android.util.TypedValue
@@ -12,7 +11,6 @@ import androidx.core.view.WindowCompat
 import coil.imageLoader
 import coil.request.Disposable
 import coil.request.ImageRequest
-import com.davemorrissey.labs.subscaleview.ImageSource
 import dev.chrisbanes.insetter.applyInsetter
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -20,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.databinding.MangaFullCoverDialogBinding
 import eu.kanade.tachiyomi.ui.base.controller.DialogController
 import eu.kanade.tachiyomi.ui.manga.MangaController
+import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
 import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
 import eu.kanade.tachiyomi.widget.TachiyomiFullscreenDialog
 import uy.kohesive.injekt.Injekt
@@ -63,12 +62,6 @@ class MangaFullCoverDialog : DialogController {
             menu?.findItem(R.id.action_edit_cover)?.isVisible = manga?.favorite ?: false
         }
 
-        binding?.fullCover?.apply {
-            setOnClickListener {
-                dialog?.dismiss()
-            }
-            setMinimumDpi(45)
-        }
         setImage(manga)
 
         binding?.appbar?.applyInsetter {
@@ -77,11 +70,10 @@ class MangaFullCoverDialog : DialogController {
             }
         }
 
-        binding?.fullCover?.applyInsetter {
+        binding?.container?.onViewClicked = { dialog?.dismiss() }
+        binding?.container?.applyInsetter {
             type(navigationBars = true) {
-                // Padding will make to image top align
-                // This is likely an issue with SubsamplingScaleImageView
-                margin(bottom = true)
+                padding(bottom = true)
             }
         }
 
@@ -108,12 +100,16 @@ class MangaFullCoverDialog : DialogController {
     }
 
     fun setImage(manga: Manga?) {
-        val manga = manga ?: return
+        if (manga == null) return
         val request = ImageRequest.Builder(applicationContext!!)
             .data(manga)
             .target {
-                val bitmap = (it as BitmapDrawable).bitmap
-                binding?.fullCover?.setImage(ImageSource.cachedBitmap(bitmap))
+                binding?.container?.setImage(
+                    it,
+                    ReaderPageImageView.Config(
+                        zoomDuration = 500
+                    )
+                )
             }
             .build()
 

+ 0 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

@@ -65,7 +65,6 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
 import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
 import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
 import eu.kanade.tachiyomi.util.storage.getUriCompat
-import eu.kanade.tachiyomi.util.system.GLUtil
 import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
 import eu.kanade.tachiyomi.util.system.createReaderThemeContext
 import eu.kanade.tachiyomi.util.system.getThemeColor
@@ -109,11 +108,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
 
     private val preferences: PreferencesHelper by injectLazy()
 
-    /**
-     * The maximum bitmap size supported by the device.
-     */
-    val maxBitmapSize by lazy { GLUtil.maxTextureSize }
-
     val hasCutout by lazy { hasDisplayCutout() }
 
     /**

+ 264 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt

@@ -0,0 +1,264 @@
+package eu.kanade.tachiyomi.ui.reader.viewer
+
+import android.content.Context
+import android.graphics.PointF
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout
+import androidx.annotation.AttrRes
+import androidx.annotation.CallSuper
+import androidx.annotation.StyleRes
+import androidx.appcompat.widget.AppCompatImageView
+import androidx.core.view.isVisible
+import coil.clear
+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.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 java.io.InputStream
+import java.nio.ByteBuffer
+
+/**
+ * A wrapper view for showing page image.
+ *
+ * Animated image will be drawn by [PhotoView] while [SubsamplingScaleImageView] will take non-animated image.
+ *
+ * @param isWebtoon if true, [WebtoonSubsamplingImageView] will be used instead of [SubsamplingScaleImageView]
+ * and [AppCompatImageView] will be used instead of [PhotoView]
+ */
+open class ReaderPageImageView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    @AttrRes defStyleAttrs: Int = 0,
+    @StyleRes defStyleRes: Int = 0,
+    private val isWebtoon: Boolean = false
+) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
+
+    private var pageView: View? = null
+
+    var onImageLoaded: (() -> Unit)? = null
+    var onImageLoadError: (() -> Unit)? = null
+    var onScaleChanged: ((newScale: Float) -> Unit)? = null
+    var onViewClicked: (() -> Unit)? = null
+
+    @CallSuper
+    open fun onImageLoaded() {
+        onImageLoaded?.invoke()
+    }
+
+    @CallSuper
+    open fun onImageLoadError() {
+        onImageLoadError?.invoke()
+    }
+
+    @CallSuper
+    open fun onScaleChanged(newScale: Float) {
+        onScaleChanged?.invoke(newScale)
+    }
+
+    @CallSuper
+    open fun onViewClicked() {
+        onViewClicked?.invoke()
+    }
+
+    fun setImage(drawable: Drawable, config: Config) {
+        if (drawable is Animatable) {
+            prepareAnimatedImageView()
+            setAnimatedImage(drawable, config)
+        } else {
+            prepareNonAnimatedImageView()
+            setNonAnimatedImage(drawable, config)
+        }
+    }
+
+    fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) {
+        if (isAnimated) {
+            prepareAnimatedImageView()
+            setAnimatedImage(inputStream, config)
+        } else {
+            prepareNonAnimatedImageView()
+            setNonAnimatedImage(inputStream, config)
+        }
+    }
+
+    fun recycle() = pageView?.let {
+        when (it) {
+            is SubsamplingScaleImageView -> it.recycle()
+            is AppCompatImageView -> it.clear()
+        }
+        it.isVisible = false
+    }
+
+    private fun prepareNonAnimatedImageView() {
+        if (pageView is SubsamplingScaleImageView) return
+        removeView(pageView)
+
+        pageView = if (isWebtoon) {
+            WebtoonSubsamplingImageView(context)
+        } else {
+            SubsamplingScaleImageView(context)
+        }.apply {
+            setMaxTileSize(GLUtil.maxTextureSize)
+            setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
+            setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
+            setMinimumTileDpi(180)
+            setOnStateChangedListener(
+                object : SubsamplingScaleImageView.OnStateChangedListener {
+                    override fun onScaleChanged(newScale: Float, origin: Int) {
+                        [email protected](newScale)
+                    }
+
+                    override fun onCenterChanged(newCenter: PointF?, origin: Int) {
+                        // Not used
+                    }
+                }
+            )
+            setOnClickListener { [email protected]() }
+        }
+        addView(pageView, MATCH_PARENT, MATCH_PARENT)
+    }
+
+    private fun setNonAnimatedImage(
+        image: Any,
+        config: Config
+    ) = (pageView as? SubsamplingScaleImageView)?.apply {
+        setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
+        setMinimumScaleType(config.minimumScaleType)
+        setMinimumDpi(1) // Just so that very small image will be fit for initial load
+        setCropBorders(config.cropBorders)
+        setOnImageEventListener(
+            object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
+                override fun onReady() {
+                    // 3x 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 })
+                    }
+                    [email protected]()
+                }
+
+                override fun onImageLoadError(e: Exception) {
+                    [email protected]()
+                }
+            }
+        )
+
+        when (image) {
+            is Drawable -> {
+                val bitmap = (image as BitmapDrawable).bitmap
+                setImage(ImageSource.bitmap(bitmap))
+            }
+            is InputStream -> setImage(ImageSource.inputStream(image))
+            else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
+        }
+        isVisible = true
+    }
+
+    private fun prepareAnimatedImageView() {
+        if (pageView is AppCompatImageView) return
+        removeView(pageView)
+
+        pageView = if (isWebtoon) {
+            AppCompatImageView(context)
+        } else {
+            PhotoView(context)
+        }.apply {
+            adjustViewBounds = true
+
+            if (this is PhotoView) {
+                setScaleLevels(1F, 2F, MAX_ZOOM_SCALE)
+                // 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
+                        }
+
+                        override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
+                            [email protected]()
+                            return super.onSingleTapConfirmed(e)
+                        }
+                    }
+                )
+                setOnScaleChangeListener { _, _, _ ->
+                    [email protected](scale)
+                }
+            }
+        }
+        addView(pageView, MATCH_PARENT, MATCH_PARENT)
+    }
+
+    private fun setAnimatedImage(
+        image: Any,
+        config: Config
+    ) = (pageView as? AppCompatImageView)?.apply {
+        if (this is PhotoView) {
+            setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration())
+        }
+
+        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
+                    [email protected]()
+                },
+                onError = {
+                    [email protected]()
+                }
+            )
+            .crossfade(false)
+            .build()
+        context.imageLoader.enqueue(request)
+    }
+
+    private fun Int.getSystemScaledDuration(): Int {
+        return (this * context.animatorDurationScale).toInt().coerceAtLeast(1)
+    }
+
+    /**
+     * All of the config except [zoomDuration] will only be used for non-animated image.
+     */
+    data class Config(
+        val zoomDuration: Int,
+        val minimumScaleType: Int = SCALE_TYPE_CENTER_INSIDE,
+        val cropBorders: Boolean = false,
+        val zoomStartPosition: ZoomStartPosition = ZoomStartPosition.CENTER
+    )
+
+    enum class ZoomStartPosition {
+        LEFT, CENTER, RIGHT
+    }
+}
+
+private const val MAX_ZOOM_SCALE = 3F

+ 8 - 11
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt

@@ -1,6 +1,7 @@
 package eu.kanade.tachiyomi.ui.reader.viewer.pager
 
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
 import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
 import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
 import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
@@ -34,7 +35,7 @@ class PagerConfig(
     var imageScaleType = 1
         private set
 
-    var imageZoomType = ZoomType.Left
+    var imageZoomType = ReaderPageImageView.ZoomStartPosition.LEFT
         private set
 
     var imageCropBorders = false
@@ -86,16 +87,16 @@ class PagerConfig(
         imageZoomType = when (value) {
             // Auto
             1 -> when (viewer) {
-                is L2RPagerViewer -> ZoomType.Left
-                is R2LPagerViewer -> ZoomType.Right
-                else -> ZoomType.Center
+                is L2RPagerViewer -> ReaderPageImageView.ZoomStartPosition.LEFT
+                is R2LPagerViewer -> ReaderPageImageView.ZoomStartPosition.RIGHT
+                else -> ReaderPageImageView.ZoomStartPosition.CENTER
             }
             // Left
-            2 -> ZoomType.Left
+            2 -> ReaderPageImageView.ZoomStartPosition.LEFT
             // Right
-            3 -> ZoomType.Right
+            3 -> ReaderPageImageView.ZoomStartPosition.RIGHT
             // Center
-            else -> ZoomType.Center
+            else -> ReaderPageImageView.ZoomStartPosition.CENTER
         }
     }
 
@@ -122,8 +123,4 @@ class PagerConfig(
         }
         navigationModeChangedListener?.invoke()
     }
-
-    enum class ZoomType {
-        Left, Center, Right
-    }
 }

+ 23 - 151
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt

@@ -1,35 +1,21 @@
 package eu.kanade.tachiyomi.ui.reader.viewer.pager
 
 import android.annotation.SuppressLint
-import android.app.ActionBar
 import android.content.Context
-import android.graphics.PointF
-import android.graphics.drawable.Animatable
-import android.view.GestureDetector
 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.widget.FrameLayout
-import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.core.view.isVisible
 import androidx.core.view.setMargins
 import androidx.core.view.updateLayoutParams
-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.source.model.Page
 import eu.kanade.tachiyomi.ui.reader.model.InsertPage
 import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
+import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
 import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
 import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 import eu.kanade.tachiyomi.util.system.ImageUtil
 import eu.kanade.tachiyomi.util.system.dpToPx
@@ -40,7 +26,6 @@ import rx.android.schedulers.AndroidSchedulers
 import rx.schedulers.Schedulers
 import java.io.ByteArrayInputStream
 import java.io.InputStream
-import java.nio.ByteBuffer
 import java.util.concurrent.TimeUnit
 
 /**
@@ -51,7 +36,7 @@ class PagerPageHolder(
     readerThemedContext: Context,
     val viewer: PagerViewer,
     val page: ReaderPage
-) : FrameLayout(readerThemedContext), ViewPagerAdapter.PositionableView {
+) : ReaderPageImageView(readerThemedContext), ViewPagerAdapter.PositionableView {
 
     /**
      * Item that identifies this view. Needed by the adapter to not recreate views.
@@ -62,17 +47,11 @@ class PagerPageHolder(
     /**
      * Loading progress bar to indicate the current progress.
      */
-    private val progressIndicator: ReaderProgressIndicator
-
-    /**
-     * Image view that supports subsampling on zoom.
-     */
-    private var subsamplingImageView: SubsamplingScaleImageView? = null
-
-    /**
-     * Simple image view only used on GIFs.
-     */
-    private var imageView: ImageView? = null
+    private val progressIndicator: ReaderProgressIndicator = ReaderProgressIndicator(readerThemedContext).apply {
+        updateLayoutParams<LayoutParams> {
+            gravity = Gravity.CENTER
+        }
+    }
 
     /**
      * Retry button used to allow retrying.
@@ -100,36 +79,9 @@ class PagerPageHolder(
      */
     private var readImageHeaderSubscription: Subscription? = null
 
-    val stateChangedListener = object : SubsamplingScaleImageView.OnStateChangedListener {
-        override fun onScaleChanged(newScale: Float, origin: Int) {
-            viewer.activity.hideMenu()
-        }
-
-        override fun onCenterChanged(newCenter: PointF?, origin: Int) {
-            viewer.activity.hideMenu()
-        }
-    }
-    private var visibilityListener = ActionBar.OnMenuVisibilityListener { isVisible ->
-        if (isVisible.not()) {
-            subsamplingImageView?.setOnStateChangedListener(null)
-            return@OnMenuVisibilityListener
-        }
-        subsamplingImageView?.setOnStateChangedListener(stateChangedListener)
-    }
-
     init {
-        progressIndicator = ReaderProgressIndicator(readerThemedContext).apply {
-            updateLayoutParams<LayoutParams> {
-                gravity = Gravity.CENTER
-            }
-        }
         addView(progressIndicator)
         observeStatus()
-        viewer.activity.addOnMenuVisibilityListener(visibilityListener)
-        if (viewer.activity.menuVisible) {
-            // Listener will not be available if user changed page with seek bar
-            subsamplingImageView?.setOnStateChangedListener(stateChangedListener)
-        }
     }
 
     /**
@@ -141,9 +93,6 @@ class PagerPageHolder(
         unsubscribeProgress()
         unsubscribeStatus()
         unsubscribeReadImageHeader()
-        subsamplingImageView?.setOnImageEventListener(null)
-        subsamplingImageView?.setOnStateChangedListener(null)
-        viewer.activity.removeOnMenuVisibilityListener(visibilityListener)
     }
 
     /**
@@ -284,13 +233,18 @@ class PagerPageHolder(
             .observeOn(AndroidSchedulers.mainThread())
             .doOnNext { (bais, isAnimated, background) ->
                 bais.use {
+                    setImage(
+                        it,
+                        isAnimated,
+                        Config(
+                            zoomDuration = viewer.config.doubleTapAnimDuration,
+                            minimumScaleType = viewer.config.imageScaleType,
+                            cropBorders = viewer.config.imageCropBorders,
+                            zoomStartPosition = viewer.config.imageZoomType
+                        )
+                    )
                     if (!isAnimated) {
                         this.background = background
-                        initSubsamplingImageView().apply {
-                            setImage(ImageSource.inputStream(it))
-                        }
-                    } else {
-                        initImageView().setImage(it)
                     }
                 }
             }
@@ -351,76 +305,18 @@ class PagerPageHolder(
     /**
      * Called when an image fails to decode.
      */
-    private fun onImageDecodeError() {
+    override fun onImageLoadError() {
+        super.onImageLoadError()
         progressIndicator.hide()
         initDecodeErrorLayout().isVisible = true
     }
 
     /**
-     * Initializes a subsampling scale view.
+     * Called when an image is zoomed in/out.
      */
-    private fun initSubsamplingImageView(): SubsamplingScaleImageView {
-        if (subsamplingImageView != null) return subsamplingImageView!!
-
-        val config = viewer.config
-
-        subsamplingImageView = SubsamplingScaleImageView(context).apply {
-            layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
-            setMaxTileSize(viewer.activity.maxBitmapSize)
-            setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
-            setDoubleTapZoomDuration(config.doubleTapAnimDuration)
-            setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
-            setMinimumScaleType(config.imageScaleType)
-            setMinimumDpi(90)
-            setMinimumTileDpi(180)
-            setCropBorders(config.imageCropBorders)
-            setOnImageEventListener(
-                object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
-                    override fun onReady() {
-                        when (config.imageZoomType) {
-                            ZoomType.Left -> setScaleAndCenter(scale, PointF(0f, 0f))
-                            ZoomType.Right -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f))
-                            ZoomType.Center -> setScaleAndCenter(scale, center.also { it?.y = 0f })
-                        }
-                    }
-
-                    override fun onImageLoadError(e: Exception) {
-                        onImageDecodeError()
-                    }
-                }
-            )
-        }
-        addView(subsamplingImageView)
-        return subsamplingImageView!!
-    }
-
-    /**
-     * Initializes an image view, used for GIFs.
-     */
-    private fun initImageView(): ImageView {
-        if (imageView != null) return imageView!!
-
-        imageView = PhotoView(context, null).apply {
-            layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
-            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)
-        return imageView!!
+    override fun onScaleChanged(newScale: Float) {
+        super.onScaleChanged(newScale)
+        viewer.activity.hideMenu()
     }
 
     /**
@@ -497,28 +393,4 @@ class PagerPageHolder(
         addView(decodeLayout)
         return decodeLayout
     }
-
-    /**
-     * Extension method to set a [stream] into this ImageView.
-     */
-    private fun ImageView.setImage(stream: InputStream) {
-        val request = ImageRequest.Builder(context)
-            .data(ByteBuffer.wrap(stream.readBytes()))
-            .memoryCachePolicy(CachePolicy.DISABLED)
-            .diskCachePolicy(CachePolicy.DISABLED)
-            .target(
-                onSuccess = { result ->
-                    if (result is Animatable) {
-                        result.start()
-                    }
-                    setImageDrawable(result)
-                },
-                onError = {
-                    onImageDecodeError()
-                }
-            )
-            .crossfade(false)
-            .build()
-        context.imageLoader.enqueue(request)
-    }
 }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt

@@ -1,7 +1,6 @@
 package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
 
 import android.view.ViewGroup
-import android.widget.FrameLayout
 import android.widget.LinearLayout
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.RecyclerView
@@ -9,6 +8,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
 import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
 import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
 import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
+import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
 import eu.kanade.tachiyomi.ui.reader.viewer.hasMissingChapters
 import eu.kanade.tachiyomi.util.system.createReaderThemeContext
 
@@ -112,7 +112,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
         return when (viewType) {
             PAGE_VIEW -> {
-                val view = FrameLayout(readerThemedContext)
+                val view = ReaderPageImageView(readerThemedContext, isWebtoon = true)
                 WebtoonPageHolder(view, viewer)
             }
             TRANSITION_VIEW -> {

+ 16 - 111
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt

@@ -1,29 +1,22 @@
 package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
 
 import android.content.res.Resources
-import android.graphics.drawable.Animatable
 import android.view.Gravity
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.FrameLayout
-import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.appcompat.widget.AppCompatButton
-import androidx.appcompat.widget.AppCompatImageView
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
 import androidx.core.view.updateMargins
-import coil.clear
-import coil.imageLoader
-import coil.request.CachePolicy
-import coil.request.ImageRequest
-import com.davemorrissey.labs.subscaleview.ImageSource
 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
 import eu.kanade.tachiyomi.R
 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.ReaderProgressIndicator
 import eu.kanade.tachiyomi.ui.webview.WebViewActivity
 import eu.kanade.tachiyomi.util.system.ImageUtil
@@ -33,7 +26,6 @@ import rx.Subscription
 import rx.android.schedulers.AndroidSchedulers
 import rx.schedulers.Schedulers
 import java.io.InputStream
-import java.nio.ByteBuffer
 import java.util.concurrent.TimeUnit
 
 /**
@@ -44,7 +36,7 @@ import java.util.concurrent.TimeUnit
  * @constructor creates a new webtoon holder.
  */
 class WebtoonPageHolder(
-    private val frame: FrameLayout,
+    private val frame: ReaderPageImageView,
     viewer: WebtoonViewer
 ) : WebtoonBaseHolder(frame, viewer) {
 
@@ -59,17 +51,6 @@ class WebtoonPageHolder(
      */
     private lateinit var progressContainer: ViewGroup
 
-    /**
-     * Image view that supports subsampling on zoom.
-     */
-    private var subsamplingImageView: SubsamplingScaleImageView? = null
-    private var cropBorders: Boolean = false
-
-    /**
-     * Simple image view only used on GIFs.
-     */
-    private var imageView: ImageView? = null
-
     /**
      * Retry button container used to allow retrying.
      */
@@ -109,6 +90,10 @@ class WebtoonPageHolder(
 
     init {
         refreshLayoutParams()
+
+        frame.onImageLoaded = { onImageDecoded() }
+        frame.onImageLoadError = { onImageDecodeError() }
+        frame.onScaleChanged = { viewer.activity.hideMenu() }
     }
 
     /**
@@ -141,10 +126,7 @@ class WebtoonPageHolder(
         unsubscribeReadImageHeader()
 
         removeDecodeErrorLayout()
-        subsamplingImageView?.recycle()
-        subsamplingImageView?.isVisible = false
-        imageView?.clear()
-        imageView?.isVisible = false
+        frame.recycle()
         progressIndicator.setProgress(0, animated = false)
     }
 
@@ -283,15 +265,15 @@ class WebtoonPageHolder(
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
             .doOnNext { isAnimated ->
-                if (!isAnimated) {
-                    val subsamplingView = initSubsamplingImageView()
-                    subsamplingView.isVisible = true
-                    subsamplingView.setImage(ImageSource.inputStream(openStream!!))
-                } else {
-                    val imageView = initImageView()
-                    imageView.isVisible = true
-                    imageView.setImage(openStream!!)
-                }
+                frame.setImage(
+                    openStream!!,
+                    isAnimated,
+                    ReaderPageImageView.Config(
+                        zoomDuration = viewer.config.doubleTapAnimDuration,
+                        minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH,
+                        cropBorders = viewer.config.imageCropBorders
+                    )
+                )
             }
             // Keep the Rx stream alive to close the input stream only when unsubscribed
             .flatMap { Observable.never<Unit>() }
@@ -355,58 +337,6 @@ class WebtoonPageHolder(
         return progress
     }
 
-    /**
-     * Initializes a subsampling scale view.
-     */
-    private fun initSubsamplingImageView(): SubsamplingScaleImageView {
-        val config = viewer.config
-
-        if (subsamplingImageView != null) {
-            if (config.imageCropBorders != cropBorders) {
-                cropBorders = config.imageCropBorders
-                subsamplingImageView!!.setCropBorders(config.imageCropBorders)
-            }
-
-            return subsamplingImageView!!
-        }
-
-        cropBorders = config.imageCropBorders
-        subsamplingImageView = WebtoonSubsamplingImageView(context).apply {
-            setMaxTileSize(viewer.activity.maxBitmapSize)
-            setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
-            setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
-            setMinimumDpi(90)
-            setMinimumTileDpi(180)
-            setCropBorders(cropBorders)
-            setOnImageEventListener(
-                object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
-                    override fun onReady() {
-                        onImageDecoded()
-                    }
-
-                    override fun onImageLoadError(e: Exception) {
-                        onImageDecodeError()
-                    }
-                }
-            )
-        }
-        frame.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 = AppCompatImageView(context).apply {
-            adjustViewBounds = true
-        }
-        frame.addView(imageView, MATCH_PARENT, MATCH_PARENT)
-        return imageView!!
-    }
-
     /**
      * Initializes a button to retry pages.
      */
@@ -500,29 +430,4 @@ class WebtoonPageHolder(
             decodeErrorLayout = null
         }
     }
-
-    /**
-     * Extension method to set a [stream] into this ImageView.
-     */
-    private fun ImageView.setImage(stream: InputStream) {
-        val request = ImageRequest.Builder(context)
-            .data(ByteBuffer.wrap(stream.readBytes()))
-            .memoryCachePolicy(CachePolicy.DISABLED)
-            .diskCachePolicy(CachePolicy.DISABLED)
-            .target(
-                onSuccess = { result ->
-                    if (result is Animatable) {
-                        result.start()
-                    }
-                    setImageDrawable(result)
-                    onImageDecoded()
-                },
-                onError = {
-                    onImageDecodeError()
-                }
-            )
-            .crossfade(false)
-            .build()
-        context.imageLoader.enqueue(request)
-    }
 }

+ 4 - 2
app/src/main/res/layout/manga_full_cover_dialog.xml

@@ -23,10 +23,12 @@
 
     </com.google.android.material.appbar.AppBarLayout>
 
-    <com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
-        android:id="@+id/full_cover"
+    <eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
+        android:id="@+id/container"
         android:layout_width="0dp"
         android:layout_height="0dp"
+        android:clipToPadding="false"
+        android:clipChildren="false"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"