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 package eu.kanade.tachiyomi.ui.manga.info
import android.app.Dialog import android.app.Dialog
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
@ -12,7 +11,6 @@ import androidx.core.view.WindowCompat
import coil.imageLoader import coil.imageLoader
import coil.request.Disposable import coil.request.Disposable
import coil.request.ImageRequest import coil.request.ImageRequest
import com.davemorrissey.labs.subscaleview.ImageSource
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper 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.databinding.MangaFullCoverDialogBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.manga.MangaController 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.util.view.setNavigationBarTransparentCompat
import eu.kanade.tachiyomi.widget.TachiyomiFullscreenDialog import eu.kanade.tachiyomi.widget.TachiyomiFullscreenDialog
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -63,12 +62,6 @@ class MangaFullCoverDialog : DialogController {
menu?.findItem(R.id.action_edit_cover)?.isVisible = manga?.favorite ?: false menu?.findItem(R.id.action_edit_cover)?.isVisible = manga?.favorite ?: false
} }
binding?.fullCover?.apply {
setOnClickListener {
dialog?.dismiss()
}
setMinimumDpi(45)
}
setImage(manga) setImage(manga)
binding?.appbar?.applyInsetter { binding?.appbar?.applyInsetter {
@ -77,11 +70,10 @@ class MangaFullCoverDialog : DialogController {
} }
} }
binding?.fullCover?.applyInsetter { binding?.container?.onViewClicked = { dialog?.dismiss() }
binding?.container?.applyInsetter {
type(navigationBars = true) { type(navigationBars = true) {
// Padding will make to image top align padding(bottom = true)
// This is likely an issue with SubsamplingScaleImageView
margin(bottom = true)
} }
} }
@ -108,12 +100,16 @@ class MangaFullCoverDialog : DialogController {
} }
fun setImage(manga: Manga?) { fun setImage(manga: Manga?) {
val manga = manga ?: return if (manga == null) return
val request = ImageRequest.Builder(applicationContext!!) val request = ImageRequest.Builder(applicationContext!!)
.data(manga) .data(manga)
.target { .target {
val bitmap = (it as BitmapDrawable).bitmap binding?.container?.setImage(
binding?.fullCover?.setImage(ImageSource.cachedBitmap(bitmap)) it,
ReaderPageImageView.Config(
zoomDuration = 500
)
)
} }
.build() .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.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.util.storage.getUriCompat 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.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.createReaderThemeContext import eu.kanade.tachiyomi.util.system.createReaderThemeContext
import eu.kanade.tachiyomi.util.system.getThemeColor import eu.kanade.tachiyomi.util.system.getThemeColor
@ -109,11 +108,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
private val preferences: PreferencesHelper by injectLazy() 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() } 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 package eu.kanade.tachiyomi.ui.reader.viewer.pager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.ViewerConfig
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
@ -34,7 +35,7 @@ class PagerConfig(
var imageScaleType = 1 var imageScaleType = 1
private set private set
var imageZoomType = ZoomType.Left var imageZoomType = ReaderPageImageView.ZoomStartPosition.LEFT
private set private set
var imageCropBorders = false var imageCropBorders = false
@ -86,16 +87,16 @@ class PagerConfig(
imageZoomType = when (value) { imageZoomType = when (value) {
// Auto // Auto
1 -> when (viewer) { 1 -> when (viewer) {
is L2RPagerViewer -> ZoomType.Left is L2RPagerViewer -> ReaderPageImageView.ZoomStartPosition.LEFT
is R2LPagerViewer -> ZoomType.Right is R2LPagerViewer -> ReaderPageImageView.ZoomStartPosition.RIGHT
else -> ZoomType.Center else -> ReaderPageImageView.ZoomStartPosition.CENTER
} }
// Left // Left
2 -> ZoomType.Left 2 -> ReaderPageImageView.ZoomStartPosition.LEFT
// Right // Right
3 -> ZoomType.Right 3 -> ReaderPageImageView.ZoomStartPosition.RIGHT
// Center // Center
else -> ZoomType.Center else -> ReaderPageImageView.ZoomStartPosition.CENTER
} }
} }
@ -122,8 +123,4 @@ class PagerConfig(
} }
navigationModeChangedListener?.invoke() navigationModeChangedListener?.invoke()
} }
enum class ZoomType {
Left, Center, Right
}
} }

View file

@ -1,35 +1,21 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.ActionBar
import android.content.Context import android.content.Context
import android.graphics.PointF
import android.graphics.drawable.Animatable
import android.view.GestureDetector
import android.view.Gravity import android.view.Gravity
import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.setMargins import androidx.core.view.setMargins
import androidx.core.view.updateLayoutParams 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.R
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.InsertPage import eu.kanade.tachiyomi.ui.reader.model.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage 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.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
@ -40,7 +26,6 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@ -51,7 +36,7 @@ class PagerPageHolder(
readerThemedContext: Context, readerThemedContext: Context,
val viewer: PagerViewer, val viewer: PagerViewer,
val page: ReaderPage val page: ReaderPage
) : FrameLayout(readerThemedContext), ViewPagerAdapter.PositionableView { ) : ReaderPageImageView(readerThemedContext), ViewPagerAdapter.PositionableView {
/** /**
* Item that identifies this view. Needed by the adapter to not recreate views. * 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. * Loading progress bar to indicate the current progress.
*/ */
private val progressIndicator: ReaderProgressIndicator private val progressIndicator: ReaderProgressIndicator = ReaderProgressIndicator(readerThemedContext).apply {
updateLayoutParams<LayoutParams> {
/** gravity = Gravity.CENTER
* 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. * Retry button used to allow retrying.
@ -100,36 +79,9 @@ class PagerPageHolder(
*/ */
private var readImageHeaderSubscription: Subscription? = null 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 { init {
progressIndicator = ReaderProgressIndicator(readerThemedContext).apply {
updateLayoutParams<LayoutParams> {
gravity = Gravity.CENTER
}
}
addView(progressIndicator) addView(progressIndicator)
observeStatus() 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() unsubscribeProgress()
unsubscribeStatus() unsubscribeStatus()
unsubscribeReadImageHeader() unsubscribeReadImageHeader()
subsamplingImageView?.setOnImageEventListener(null)
subsamplingImageView?.setOnStateChangedListener(null)
viewer.activity.removeOnMenuVisibilityListener(visibilityListener)
} }
/** /**
@ -284,13 +233,18 @@ class PagerPageHolder(
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { (bais, isAnimated, background) -> .doOnNext { (bais, isAnimated, background) ->
bais.use { bais.use {
setImage(
it,
isAnimated,
Config(
zoomDuration = viewer.config.doubleTapAnimDuration,
minimumScaleType = viewer.config.imageScaleType,
cropBorders = viewer.config.imageCropBorders,
zoomStartPosition = viewer.config.imageZoomType
)
)
if (!isAnimated) { if (!isAnimated) {
this.background = background 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. * Called when an image fails to decode.
*/ */
private fun onImageDecodeError() { override fun onImageLoadError() {
super.onImageLoadError()
progressIndicator.hide() progressIndicator.hide()
initDecodeErrorLayout().isVisible = true initDecodeErrorLayout().isVisible = true
} }
/** /**
* Initializes a subsampling scale view. * Called when an image is zoomed in/out.
*/ */
private fun initSubsamplingImageView(): SubsamplingScaleImageView { override fun onScaleChanged(newScale: Float) {
if (subsamplingImageView != null) return subsamplingImageView!! super.onScaleChanged(newScale)
viewer.activity.hideMenu()
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!!
} }
/** /**
@ -497,28 +393,4 @@ class PagerPageHolder(
addView(decodeLayout) addView(decodeLayout)
return 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 package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView 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.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters 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.ui.reader.viewer.hasMissingChapters
import eu.kanade.tachiyomi.util.system.createReaderThemeContext 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) { return when (viewType) {
PAGE_VIEW -> { PAGE_VIEW -> {
val view = FrameLayout(readerThemedContext) val view = ReaderPageImageView(readerThemedContext, isWebtoon = true)
WebtoonPageHolder(view, viewer) WebtoonPageHolder(view, viewer)
} }
TRANSITION_VIEW -> { TRANSITION_VIEW -> {

View file

@ -1,29 +1,22 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.Animatable
import android.view.Gravity import android.view.Gravity
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins 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 com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage 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.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
@ -33,7 +26,6 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import java.io.InputStream import java.io.InputStream
import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@ -44,7 +36,7 @@ import java.util.concurrent.TimeUnit
* @constructor creates a new webtoon holder. * @constructor creates a new webtoon holder.
*/ */
class WebtoonPageHolder( class WebtoonPageHolder(
private val frame: FrameLayout, private val frame: ReaderPageImageView,
viewer: WebtoonViewer viewer: WebtoonViewer
) : WebtoonBaseHolder(frame, viewer) { ) : WebtoonBaseHolder(frame, viewer) {
@ -59,17 +51,6 @@ class WebtoonPageHolder(
*/ */
private lateinit var progressContainer: ViewGroup 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. * Retry button container used to allow retrying.
*/ */
@ -109,6 +90,10 @@ class WebtoonPageHolder(
init { init {
refreshLayoutParams() refreshLayoutParams()
frame.onImageLoaded = { onImageDecoded() }
frame.onImageLoadError = { onImageDecodeError() }
frame.onScaleChanged = { viewer.activity.hideMenu() }
} }
/** /**
@ -141,10 +126,7 @@ class WebtoonPageHolder(
unsubscribeReadImageHeader() unsubscribeReadImageHeader()
removeDecodeErrorLayout() removeDecodeErrorLayout()
subsamplingImageView?.recycle() frame.recycle()
subsamplingImageView?.isVisible = false
imageView?.clear()
imageView?.isVisible = false
progressIndicator.setProgress(0, animated = false) progressIndicator.setProgress(0, animated = false)
} }
@ -283,15 +265,15 @@ class WebtoonPageHolder(
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated -> .doOnNext { isAnimated ->
if (!isAnimated) { frame.setImage(
val subsamplingView = initSubsamplingImageView() openStream!!,
subsamplingView.isVisible = true isAnimated,
subsamplingView.setImage(ImageSource.inputStream(openStream!!)) ReaderPageImageView.Config(
} else { zoomDuration = viewer.config.doubleTapAnimDuration,
val imageView = initImageView() minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH,
imageView.isVisible = true cropBorders = viewer.config.imageCropBorders
imageView.setImage(openStream!!) )
} )
} }
// Keep the Rx stream alive to close the input stream only when unsubscribed // Keep the Rx stream alive to close the input stream only when unsubscribed
.flatMap { Observable.never<Unit>() } .flatMap { Observable.never<Unit>() }
@ -355,58 +337,6 @@ class WebtoonPageHolder(
return progress 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. * Initializes a button to retry pages.
*/ */
@ -500,29 +430,4 @@ class WebtoonPageHolder(
decodeErrorLayout = null 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.google.android.material.appbar.AppBarLayout>
<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView <eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
android:id="@+id/full_cover" android:id="@+id/container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:clipToPadding="false"
android:clipChildren="false"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"