diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFullCoverDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFullCoverDialog.kt index aaa4ca965..207169c47 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFullCoverDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFullCoverDialog.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.manga.info import android.app.Dialog -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.util.TypedValue @@ -12,7 +11,6 @@ import androidx.core.view.WindowCompat import coil.imageLoader import coil.request.Disposable import coil.request.ImageRequest -import com.davemorrissey.labs.subscaleview.ImageSource import dev.chrisbanes.insetter.applyInsetter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -20,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.databinding.MangaFullCoverDialogBinding import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat import eu.kanade.tachiyomi.widget.TachiyomiFullscreenDialog import uy.kohesive.injekt.Injekt @@ -63,12 +62,6 @@ class MangaFullCoverDialog : DialogController { menu?.findItem(R.id.action_edit_cover)?.isVisible = manga?.favorite ?: false } - binding?.fullCover?.apply { - setOnClickListener { - dialog?.dismiss() - } - setMinimumDpi(45) - } setImage(manga) binding?.appbar?.applyInsetter { @@ -77,11 +70,10 @@ class MangaFullCoverDialog : DialogController { } } - binding?.fullCover?.applyInsetter { + binding?.container?.onViewClicked = { dialog?.dismiss() } + binding?.container?.applyInsetter { type(navigationBars = true) { - // Padding will make to image top align - // This is likely an issue with SubsamplingScaleImageView - margin(bottom = true) + padding(bottom = true) } } @@ -108,12 +100,16 @@ class MangaFullCoverDialog : DialogController { } fun setImage(manga: Manga?) { - val manga = manga ?: return + if (manga == null) return val request = ImageRequest.Builder(applicationContext!!) .data(manga) .target { - val bitmap = (it as BitmapDrawable).bitmap - binding?.fullCover?.setImage(ImageSource.cachedBitmap(bitmap)) + binding?.container?.setImage( + it, + ReaderPageImageView.Config( + zoomDuration = 500 + ) + ) } .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index b568f7caa..ec08e3724 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -65,7 +65,6 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.util.storage.getUriCompat -import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale import eu.kanade.tachiyomi.util.system.createReaderThemeContext import eu.kanade.tachiyomi.util.system.getThemeColor @@ -109,11 +108,6 @@ class ReaderActivity : BaseRxActivity() 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() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt new file mode 100644 index 000000000..8806bd09e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -0,0 +1,264 @@ +package eu.kanade.tachiyomi.ui.reader.viewer + +import android.content.Context +import android.graphics.PointF +import android.graphics.drawable.Animatable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import androidx.annotation.AttrRes +import androidx.annotation.CallSuper +import androidx.annotation.StyleRes +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.view.isVisible +import coil.clear +import coil.imageLoader +import coil.request.CachePolicy +import coil.request.ImageRequest +import com.davemorrissey.labs.subscaleview.ImageSource +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE +import com.github.chrisbanes.photoview.PhotoView +import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView +import eu.kanade.tachiyomi.util.system.GLUtil +import eu.kanade.tachiyomi.util.system.animatorDurationScale +import java.io.InputStream +import java.nio.ByteBuffer + +/** + * A wrapper view for showing page image. + * + * Animated image will be drawn by [PhotoView] while [SubsamplingScaleImageView] will take non-animated image. + * + * @param isWebtoon if true, [WebtoonSubsamplingImageView] will be used instead of [SubsamplingScaleImageView] + * and [AppCompatImageView] will be used instead of [PhotoView] + */ +open class ReaderPageImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttrs: Int = 0, + @StyleRes defStyleRes: Int = 0, + private val isWebtoon: Boolean = false +) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) { + + private var pageView: View? = null + + var onImageLoaded: (() -> Unit)? = null + var onImageLoadError: (() -> Unit)? = null + var onScaleChanged: ((newScale: Float) -> Unit)? = null + var onViewClicked: (() -> Unit)? = null + + @CallSuper + open fun onImageLoaded() { + onImageLoaded?.invoke() + } + + @CallSuper + open fun onImageLoadError() { + onImageLoadError?.invoke() + } + + @CallSuper + open fun onScaleChanged(newScale: Float) { + onScaleChanged?.invoke(newScale) + } + + @CallSuper + open fun onViewClicked() { + onViewClicked?.invoke() + } + + fun setImage(drawable: Drawable, config: Config) { + if (drawable is Animatable) { + prepareAnimatedImageView() + setAnimatedImage(drawable, config) + } else { + prepareNonAnimatedImageView() + setNonAnimatedImage(drawable, config) + } + } + + fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) { + if (isAnimated) { + prepareAnimatedImageView() + setAnimatedImage(inputStream, config) + } else { + prepareNonAnimatedImageView() + setNonAnimatedImage(inputStream, config) + } + } + + fun recycle() = pageView?.let { + when (it) { + is SubsamplingScaleImageView -> it.recycle() + is AppCompatImageView -> it.clear() + } + it.isVisible = false + } + + private fun prepareNonAnimatedImageView() { + if (pageView is SubsamplingScaleImageView) return + removeView(pageView) + + pageView = if (isWebtoon) { + WebtoonSubsamplingImageView(context) + } else { + SubsamplingScaleImageView(context) + }.apply { + setMaxTileSize(GLUtil.maxTextureSize) + setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER) + setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) + setMinimumTileDpi(180) + setOnStateChangedListener( + object : SubsamplingScaleImageView.OnStateChangedListener { + override fun onScaleChanged(newScale: Float, origin: Int) { + 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt index 6acfc4519..fe64dd616 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation @@ -34,7 +35,7 @@ class PagerConfig( var imageScaleType = 1 private set - var imageZoomType = ZoomType.Left + var imageZoomType = ReaderPageImageView.ZoomStartPosition.LEFT private set var imageCropBorders = false @@ -86,16 +87,16 @@ class PagerConfig( imageZoomType = when (value) { // Auto 1 -> when (viewer) { - is L2RPagerViewer -> ZoomType.Left - is R2LPagerViewer -> ZoomType.Right - else -> ZoomType.Center + is L2RPagerViewer -> ReaderPageImageView.ZoomStartPosition.LEFT + is R2LPagerViewer -> ReaderPageImageView.ZoomStartPosition.RIGHT + else -> ReaderPageImageView.ZoomStartPosition.CENTER } // Left - 2 -> ZoomType.Left + 2 -> ReaderPageImageView.ZoomStartPosition.LEFT // Right - 3 -> ZoomType.Right + 3 -> ReaderPageImageView.ZoomStartPosition.RIGHT // Center - else -> ZoomType.Center + else -> ReaderPageImageView.ZoomStartPosition.CENTER } } @@ -122,8 +123,4 @@ class PagerConfig( } navigationModeChangedListener?.invoke() } - - enum class ZoomType { - Left, Center, Right - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index bd071660f..80d4ac06d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -1,35 +1,21 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager import android.annotation.SuppressLint -import android.app.ActionBar import android.content.Context -import android.graphics.PointF -import android.graphics.drawable.Animatable -import android.view.GestureDetector import android.view.Gravity -import android.view.MotionEvent import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.widget.FrameLayout -import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.core.view.isVisible import androidx.core.view.setMargins import androidx.core.view.updateLayoutParams -import coil.imageLoader -import coil.request.CachePolicy -import coil.request.ImageRequest -import com.davemorrissey.labs.subscaleview.ImageSource -import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView -import com.github.chrisbanes.photoview.PhotoView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.InsertPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator -import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.dpToPx @@ -40,7 +26,6 @@ import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import java.io.ByteArrayInputStream import java.io.InputStream -import java.nio.ByteBuffer import java.util.concurrent.TimeUnit /** @@ -51,7 +36,7 @@ class PagerPageHolder( readerThemedContext: Context, val viewer: PagerViewer, val page: ReaderPage -) : FrameLayout(readerThemedContext), ViewPagerAdapter.PositionableView { +) : ReaderPageImageView(readerThemedContext), ViewPagerAdapter.PositionableView { /** * Item that identifies this view. Needed by the adapter to not recreate views. @@ -62,17 +47,11 @@ class PagerPageHolder( /** * Loading progress bar to indicate the current progress. */ - private val progressIndicator: ReaderProgressIndicator - - /** - * Image view that supports subsampling on zoom. - */ - private var subsamplingImageView: SubsamplingScaleImageView? = null - - /** - * Simple image view only used on GIFs. - */ - private var imageView: ImageView? = null + private val progressIndicator: ReaderProgressIndicator = ReaderProgressIndicator(readerThemedContext).apply { + updateLayoutParams { + 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 { - 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) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt index 5379090dc..4a484035f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon import android.view.ViewGroup -import android.widget.FrameLayout import android.widget.LinearLayout import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -9,6 +8,7 @@ import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.hasMissingChapters import eu.kanade.tachiyomi.util.system.createReaderThemeContext @@ -112,7 +112,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter { - val view = FrameLayout(readerThemedContext) + val view = ReaderPageImageView(readerThemedContext, isWebtoon = true) WebtoonPageHolder(view, viewer) } TRANSITION_VIEW -> { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 009b36b18..6f4e670be 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -1,29 +1,22 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon import android.content.res.Resources -import android.graphics.drawable.Animatable import android.view.Gravity import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout -import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.widget.AppCompatButton -import androidx.appcompat.widget.AppCompatImageView import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updateMargins -import coil.clear -import coil.imageLoader -import coil.request.CachePolicy -import coil.request.ImageRequest -import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.util.system.ImageUtil @@ -33,7 +26,6 @@ import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import java.io.InputStream -import java.nio.ByteBuffer import java.util.concurrent.TimeUnit /** @@ -44,7 +36,7 @@ import java.util.concurrent.TimeUnit * @constructor creates a new webtoon holder. */ class WebtoonPageHolder( - private val frame: FrameLayout, + private val frame: ReaderPageImageView, viewer: WebtoonViewer ) : WebtoonBaseHolder(frame, viewer) { @@ -59,17 +51,6 @@ class WebtoonPageHolder( */ private lateinit var progressContainer: ViewGroup - /** - * Image view that supports subsampling on zoom. - */ - private var subsamplingImageView: SubsamplingScaleImageView? = null - private var cropBorders: Boolean = false - - /** - * Simple image view only used on GIFs. - */ - private var imageView: ImageView? = null - /** * Retry button container used to allow retrying. */ @@ -109,6 +90,10 @@ class WebtoonPageHolder( init { refreshLayoutParams() + + frame.onImageLoaded = { onImageDecoded() } + frame.onImageLoadError = { onImageDecodeError() } + frame.onScaleChanged = { viewer.activity.hideMenu() } } /** @@ -141,10 +126,7 @@ class WebtoonPageHolder( unsubscribeReadImageHeader() removeDecodeErrorLayout() - subsamplingImageView?.recycle() - subsamplingImageView?.isVisible = false - imageView?.clear() - imageView?.isVisible = false + frame.recycle() progressIndicator.setProgress(0, animated = false) } @@ -283,15 +265,15 @@ class WebtoonPageHolder( .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnNext { isAnimated -> - if (!isAnimated) { - val subsamplingView = initSubsamplingImageView() - subsamplingView.isVisible = true - subsamplingView.setImage(ImageSource.inputStream(openStream!!)) - } else { - val imageView = initImageView() - imageView.isVisible = true - imageView.setImage(openStream!!) - } + frame.setImage( + openStream!!, + isAnimated, + ReaderPageImageView.Config( + zoomDuration = viewer.config.doubleTapAnimDuration, + minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH, + cropBorders = viewer.config.imageCropBorders + ) + ) } // Keep the Rx stream alive to close the input stream only when unsubscribed .flatMap { Observable.never() } @@ -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) - } } diff --git a/app/src/main/res/layout/manga_full_cover_dialog.xml b/app/src/main/res/layout/manga_full_cover_dialog.xml index ce0f181e9..982ccb753 100644 --- a/app/src/main/res/layout/manga_full_cover_dialog.xml +++ b/app/src/main/res/layout/manga_full_cover_dialog.xml @@ -23,10 +23,12 @@ -