mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	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:
		| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user