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:
parent
9a7a03e327
commit
746d35b52b
8 changed files with 328 additions and 298 deletions
|
@ -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()
|
||||
|
||||
|
|
|
@ -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() }
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Reference in a new issue