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.
This commit is contained in:
Ivan Iskandar 2021-09-12 05:28:54 +07:00 committed by GitHub
parent 9a7a03e327
commit 746d35b52b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 328 additions and 298 deletions

View file

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

View file

@ -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() }
/**

View file

@ -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) {
this@ReaderPageImageView.onScaleChanged(newScale)
}
override fun onCenterChanged(newCenter: PointF?, origin: Int) {
// Not used
}
}
)
setOnClickListener { this@ReaderPageImageView.onViewClicked() }
}
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 })
}
this@ReaderPageImageView.onImageLoaded()
}
override fun onImageLoadError(e: Exception) {
this@ReaderPageImageView.onImageLoadError()
}
}
)
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 {
this@ReaderPageImageView.onViewClicked()
return super.onSingleTapConfirmed(e)
}
}
)
setOnScaleChangeListener { _, _, _ ->
this@ReaderPageImageView.onScaleChanged(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
this@ReaderPageImageView.onImageLoaded()
},
onError = {
this@ReaderPageImageView.onImageLoadError()
}
)
.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

View file

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

View file

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

View file

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

View file

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

View file

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