Remove SubsamplingScaleImageView

We have no reason to keep it, just use PhotoView
This commit is contained in:
tarsin committed 2023-02-17 19:03:32 +08:00
1 parent 6b0c208b75
commit 5eda7b3fc3
14 files changed
+7 -3373

No files matched your search

@@ -1,160 +0,0 @@
package com.davemorrissey.labs.subscaleview
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import com.davemorrissey.labs.subscaleview.provider.AssetInputProvider
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.davemorrissey.labs.subscaleview.provider.OpenStreamProvider
import com.davemorrissey.labs.subscaleview.provider.ResourceInputProvider
import com.davemorrissey.labs.subscaleview.provider.UriInputProvider
import java.io.InputStream
/**
* Helper class used to set the source and additional attributes from a variety of sources. Supports
* use of a bitmap, asset, resource, external file or any other URI.
*
*
* When you are using a preview image, you must set the dimensions of the full size image on the
* ImageSource object for the full size image using the [.dimensions] method.
*/
class ImageSource {
val bitmap: Bitmap?
val provider: InputProvider?
private var sWidth = 0
private var sHeight = 0
var sRegion = Rect(0, 0, 0, 0)
private set
var isCached = false
private set
private constructor(bitmap: Bitmap, cached: Boolean) {
this.bitmap = bitmap
provider = null
sWidth = bitmap.width
sHeight = bitmap.height
isCached = cached
}
private constructor(provider: InputProvider) {
bitmap = null
this.provider = provider
}
/**
* Use a region of the source image. Region must be set independently for the full size image and the preview if
* you are using one.
*
* @param sRegion the region of the source image to be displayed.
* @return this instance for chaining.
*/
fun region(sRegion: Rect): ImageSource {
this.sRegion = sRegion
setInvariants()
return this
}
/**
* Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI
* and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare
* the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset.
*
* @param sWidth width of the source image.
* @param sHeight height of the source image.
* @return this instance for chaining.
*/
fun dimensions(sWidth: Int, sHeight: Int): ImageSource {
if (bitmap == null) {
this.sWidth = sWidth
this.sHeight = sHeight
}
setInvariants()
return this
}
private fun setInvariants() {
if (!sRegion.isEmpty) {
sWidth = sRegion.width()
sHeight = sRegion.height()
}
}
companion object {
/**
* Create an instance from a resource. The correct resource for the device screen resolution will be used.
*
* @param resId resource ID.
* @return an [ImageSource] instance.
*/
@JvmStatic
fun resource(context: Context, resId: Int): ImageSource {
return ImageSource(ResourceInputProvider(context, resId))
}
/**
* Create an instance from an asset name.
*
* @param assetName asset name.
* @return an [ImageSource] instance.
*/
@JvmStatic
fun asset(context: Context, assetName: String): ImageSource {
return ImageSource(AssetInputProvider(context, assetName))
}
/**
* Create an instance from a URI.
*
* @param uri image URI.
* @return an [ImageSource] instance.
*/
fun uri(context: Context, uri: Uri): ImageSource {
return ImageSource(UriInputProvider(context, uri))
}
/**
* Create an instance from an input provider.
*
* @param provider input stream provider.
* @return an [ImageSource] instance.
*/
fun provider(provider: InputProvider): ImageSource {
return ImageSource(provider)
}
/**
* Create an instance from an input stream.
*
* @param stream open input stream.
* @return an [ImageSource] instance.
*/
fun inputStream(stream: InputStream): ImageSource {
return ImageSource(OpenStreamProvider(stream))
}
/**
* Provide a loaded bitmap for display.
*
* @param bitmap bitmap to be displayed.
* @return an [ImageSource] instance.
*/
fun bitmap(bitmap: Bitmap): ImageSource {
return ImageSource(bitmap, false)
}
/**
* Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no
* longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso
* or Volley.
*
* @param bitmap bitmap to be displayed.
* @return an [ImageSource] instance.
*/
fun cachedBitmap(bitmap: Bitmap): ImageSource {
return ImageSource(bitmap, true)
}
}
}
@@ -1,15 +0,0 @@
package com.davemorrissey.labs.subscaleview
import android.graphics.PointF
import java.io.Serializable
/**
* Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate.
*/
class ImageViewState(val scale: Float, center: PointF) : Serializable {
private val centerX: Float = center.x
private val centerY: Float = center.y
val center: PointF
get() = PointF(centerX, centerY)
}
File diff suppressed because it is too large. Load diff
@@ -1,53 +0,0 @@
package com.davemorrissey.labs.subscaleview.decoder
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import kotlin.Throws
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import java.lang.Exception
/**
* Interface for image decoding classes.
*/
interface Decoder {
/**
* Initialise the decoder. When possible, perform initial setup work once in this method. The
* dimensions of the image must be returned.
*
* @param context Application context. A reference may be held, but must be cleared on recycle.
* @param provider Provider of the image.
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
@Throws(Exception::class)
fun init(context: Context, provider: InputProvider): Point
/**
* Decode a region of the image with the given sample size. This method is called off the UI
* thread so it can safely load the image on the current thread. It is called from
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
* implementations must be thread safe. Adding `synchronized` to the method signature
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
* called concurrently.
*
* @param sRect Source image rectangle to decode.
* @param sampleSize Sample size.
* @return The decoded region. It is safe to return null if decoding fails.
*/
fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap
/**
* Status check. Should return false before initialisation and after recycle.
*
* @return true if the decoder is ready to be used.
*/
val isReady: Boolean
/**
* This method will be called when the decoder is no longer required. It should clean up any
* resources still in use.
*/
fun recycle()
}
@@ -1,64 +0,0 @@
package com.davemorrissey.labs.subscaleview.decoder
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.Rect
import kotlin.Throws
import com.davemorrissey.labs.subscaleview.provider.InputProvider
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import java.lang.Exception
class ImageDecoder @JvmOverloads constructor(
private val cropBorders: Boolean,
bitmapConfig: Bitmap.Config? = null
) : Decoder {
init {
val globalBitmapConfig = SubsamplingScaleImageView.preferredBitmapConfig
}
/**
* Initialise the decoder. When possible, perform initial setup work once in this method. The
* dimensions of the image must be returned.
*
* @param context Application context. A reference may be held, but must be cleared on recycle.
* @param provider Provider of the image.
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
@Throws(Exception::class)
override fun init(context: Context, provider: InputProvider): Point {
TODO()
}
/**
* Decode a region of the image with the given sample size. This method is called off the UI
* thread so it can safely load the image on the current thread. It is called from
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
* implementations must be thread safe. Adding `synchronized` to the method signature
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
* called concurrently.
*
* @param sRect Source image rectangle to decode.
* @param sampleSize Sample size.
* @return The decoded region. It is safe to return null if decoding fails.
*/
override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap {
TODO()
}
/**
* Status check. Should return false before initialisation and after recycle.
*
* @return true if the decoder is ready to be used.
*/
override val isReady: Boolean = TODO()
/**
* This method will be called when the decoder is no longer required. It should clean up any
* resources still in use.
*/
override fun recycle() {
TODO()
}
}
@@ -1,11 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import java.io.IOException
class AssetInputProvider(context: Context, val assetName: String) : InputProvider {
private val assets = context.assets
@Throws(IOException::class)
override fun openStream() = assets.open(assetName)
}
@@ -1,10 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import java.io.IOException
import java.io.InputStream
import kotlin.Throws
interface InputProvider {
@Throws(IOException::class)
fun openStream(): InputStream?
}
@@ -1,16 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import java.io.IOException
import java.io.InputStream
import kotlin.Throws
/**
* Provider from an already open input stream.
*
*
* Please keep in mind this provider is not reusable.
*/
class OpenStreamProvider(private val stream: InputStream) : InputProvider {
@Throws(IOException::class)
override fun openStream() = stream
}
@@ -1,11 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import java.io.IOException
class ResourceInputProvider(context: Context, val resource: Int) : InputProvider {
private val resources = context.resources
@Throws(IOException::class)
override fun openStream() = resources.openRawResource(resource)
}
@@ -1,13 +0,0 @@
package com.davemorrissey.labs.subscaleview.provider
import android.content.Context
import android.net.Uri
import java.io.IOException
import kotlin.Throws
class UriInputProvider(context: Context, val uri: Uri) : InputProvider {
private val resolver = context.contentResolver
@Throws(IOException::class)
override fun openStream() = resolver.openInputStream(uri)
}
@@ -1,10 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.viewer
import android.content.Context
import android.graphics.PointF
import android.graphics.RectF
import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.GestureDetector
@@ -16,20 +13,13 @@ import androidx.annotation.AttrRes
import androidx.annotation.CallSuper
import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.os.postDelayed
import androidx.core.view.isVisible
import coil.dispose
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.Companion.EASE_OUT_QUAD
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.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.animatorDurationScale
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
import java.io.InputStream
import java.nio.ByteBuffer
@@ -84,206 +74,21 @@ open class ReaderPageImageView @JvmOverloads constructor(
onViewClicked?.invoke()
}
open fun onPageSelected(forward: Boolean) {
with(pageView as? SubsamplingScaleImageView) {
if (this == null) return
if (isReady) {
landscapeZoom(forward)
} else {
setOnImageEventListener(
object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
override fun onReady() {
setupZoom(config)
landscapeZoom(forward)
this@ReaderPageImageView.onImageLoaded()
}
override fun onImageLoadError(e: Exception) {
onImageLoadError()
}
},
)
}
}
}
private fun SubsamplingScaleImageView.landscapeZoom(forward: Boolean) {
if (config != null && config!!.landscapeZoom && config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE && sWidth > sHeight && scale == getMinScale()) {
handler?.postDelayed(500) {
val point = when (config!!.zoomStartPosition) {
ZoomStartPosition.LEFT -> if (forward) PointF(0F, 0F) else PointF(sWidth.toFloat(), 0F)
ZoomStartPosition.RIGHT -> if (forward) PointF(sWidth.toFloat(), 0F) else PointF(0F, 0F)
ZoomStartPosition.CENTER -> center.also { it?.y = 0F }
}
val targetScale = height.toFloat() / sHeight.toFloat()
animateScaleAndCenter(targetScale, point)!!
.withDuration(500)
.withEasing(SubsamplingScaleImageView.EASE_IN_OUT_QUAD)
.withInterruptible(true)
.start()
}
}
}
open fun onPageSelected(forward: Boolean) {}
fun setImage(drawable: Drawable, config: Config) {
this.config = config
if (drawable is Animatable) {
prepareAnimatedImageView()
setAnimatedImage(drawable, config)
} else {
prepareNonAnimatedImageView()
setNonAnimatedImage(drawable, config)
}
}
fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) {
this.config = config
if (isAnimated) {
prepareAnimatedImageView()
setAnimatedImage(inputStream, config)
} else {
prepareNonAnimatedImageView()
setNonAnimatedImage(inputStream, config)
}
prepareAnimatedImageView()
setAnimatedImage(drawable, config)
}
fun recycle() = pageView?.let {
when (it) {
is SubsamplingScaleImageView -> it.recycle()
is AppCompatImageView -> it.dispose()
}
it.isVisible = false
}
/**
* Check if the image can be panned to the left
*/
fun canPanLeft(): Boolean = canPan { it.left }
/**
* Check if the image can be panned to the right
*/
fun canPanRight(): Boolean = canPan { it.right }
/**
* Check whether the image can be panned.
* @param fn a function that returns the direction to check for
*/
private fun canPan(fn: (RectF) -> Float): Boolean {
(pageView as? SubsamplingScaleImageView)?.let { view ->
RectF().let {
view.getPanRemaining(it)
return fn(it) > 1
}
}
return false
}
/**
* Pans the image to the left by a screen's width worth.
*/
fun panLeft() {
pan { center, view -> center.also { it.x -= view.width / view.scale } }
}
/**
* Pans the image to the right by a screen's width worth.
*/
fun panRight() {
pan { center, view -> center.also { it.x += view.width / view.scale } }
}
/**
* Pans the image.
* @param fn a function that computes the new center of the image
*/
private fun pan(fn: (PointF, SubsamplingScaleImageView) -> PointF) {
(pageView as? SubsamplingScaleImageView)?.let { view ->
val target = fn(view.center ?: return, view)
view.animateCenter(target)!!
.withEasing(EASE_OUT_QUAD)
.withDuration(250)
.withInterruptible(true)
.start()
}
}
private fun prepareNonAnimatedImageView() {
if (pageView is SubsamplingScaleImageView) return
removeView(pageView)
pageView = if (isWebtoon) {
WebtoonSubsamplingImageView(context)
} else {
SubsamplingScaleImageView(context)
}.apply {
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 SubsamplingScaleImageView.setupZoom(config: Config?) {
// 5x 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 })
null -> {}
}
}
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() {
setupZoom(config)
if (isVisibleOnScreen()) landscapeZoom(true)
this@ReaderPageImageView.onImageLoaded()
}
override fun onImageLoadError(e: Exception) {
this@ReaderPageImageView.onImageLoadError()
}
},
)
when (image) {
is Drawable -> {
val bitmap = (image as BitmapDrawable).bitmap
setImage(ImageSource.cachedBitmap(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)
@@ -365,7 +170,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
*/
data class Config(
val zoomDuration: Int,
val minimumScaleType: Int = SCALE_TYPE_CENTER_INSIDE,
val minimumScaleType: Int = 0,
val cropBorders: Boolean = false,
val zoomStartPosition: ZoomStartPosition = ZoomStartPosition.CENTER,
val landscapeZoom: Boolean = false,
@@ -251,12 +251,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
*/
protected open fun moveRight() {
if (pager.currentItem != adapter.count - 1) {
val holder = (currentPage as? ReaderPage)?.let { getPageHolder(it) }
if (holder != null && config.navigateToPan && holder.canPanRight()) {
holder.panRight()
} else {
pager.setCurrentItem(pager.currentItem + 1, config.usePageTransitions)
}
pager.setCurrentItem(pager.currentItem + 1, config.usePageTransitions)
}
}
@@ -265,12 +260,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
*/
protected open fun moveLeft() {
if (pager.currentItem != 0) {
val holder = (currentPage as? ReaderPage)?.let { getPageHolder(it) }
if (holder != null && config.navigateToPan && holder.canPanLeft()) {
holder.panLeft()
} else {
pager.setCurrentItem(pager.currentItem - 1, config.usePageTransitions)
}
pager.setCurrentItem(pager.currentItem - 1, config.usePageTransitions)
}
}
@@ -10,7 +10,6 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.hippo.ehviewer.databinding.ReaderErrorBinding
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@@ -200,7 +199,7 @@ class WebtoonPageHolder(
frame.setImage(drawable,
ReaderPageImageView.Config(
zoomDuration = viewer.config.doubleTapAnimDuration,
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH,
minimumScaleType = 0,
cropBorders = viewer.config.imageCropBorders,
),)
}
@@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
/**
* Implementation of subsampling scale image view that ignores all touch events, because the
* webtoon viewer handles all the gestures.
*/
class WebtoonSubsamplingImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : SubsamplingScaleImageView(context, attrs) {
override fun onTouchEvent(event: MotionEvent): Boolean {
return false
}
}