Refactor image loading
parent
bd4fecc3b6
commit
10bd46f077
@ -0,0 +1,192 @@
|
||||
package org.koitharu.kotatsu.core.image
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import coil3.ImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.request.Disposable
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.NullRequestData
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.request.allowRgb565
|
||||
import coil3.request.crossfade
|
||||
import coil3.request.lifecycle
|
||||
import coil3.request.target
|
||||
import coil3.size.Scale
|
||||
import coil3.size.Size
|
||||
import coil3.size.SizeResolver
|
||||
import coil3.size.ViewSizeResolver
|
||||
import coil3.util.CoilUtils
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import java.util.LinkedList
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
open class CoilImageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : ShapeableImageView(context, attrs, defStyleAttr), ImageRequest.Listener {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
var allowRgb565: Boolean = false
|
||||
var useExistingDrawable: Boolean = false
|
||||
var decodeRegion: Boolean = false
|
||||
var exactImageSize: Size? = null
|
||||
var crossfadeDurationFactor: Float = 1f
|
||||
|
||||
var placeholderDrawable: Drawable? = null
|
||||
var errorDrawable: Drawable? = null
|
||||
var fallbackDrawable: Drawable? = null
|
||||
|
||||
private var currentRequest: Disposable? = null
|
||||
private var currentImageData: Any = NullRequestData
|
||||
|
||||
private var listeners: MutableList<ImageRequest.Listener>? = null
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.CoilImageView, defStyleAttr) {
|
||||
allowRgb565 = getBoolean(R.styleable.CoilImageView_allowRgb565, allowRgb565)
|
||||
useExistingDrawable = getBoolean(R.styleable.CoilImageView_useExistingDrawable, useExistingDrawable)
|
||||
decodeRegion = getBoolean(R.styleable.CoilImageView_decodeRegion, decodeRegion)
|
||||
placeholderDrawable = getDrawable(R.styleable.CoilImageView_placeholderDrawable)
|
||||
errorDrawable = getDrawable(R.styleable.CoilImageView_errorDrawable)
|
||||
fallbackDrawable = getDrawable(R.styleable.CoilImageView_fallbackDrawable)
|
||||
crossfadeDurationFactor = if (getBoolean(R.styleable.CoilImageView_crossfadeEnabled, true)) {
|
||||
crossfadeDurationFactor
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(request: ImageRequest) {
|
||||
super.onCancel(request)
|
||||
listeners?.forEach { it.onCancel(request) }
|
||||
}
|
||||
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
super.onError(request, result)
|
||||
listeners?.forEach { it.onError(request, result) }
|
||||
}
|
||||
|
||||
override fun onStart(request: ImageRequest) {
|
||||
super.onStart(request)
|
||||
listeners?.forEach { it.onStart(request) }
|
||||
}
|
||||
|
||||
override fun onSuccess(request: ImageRequest, result: SuccessResult) {
|
||||
super.onSuccess(request, result)
|
||||
listeners?.forEach { it.onSuccess(request, result) }
|
||||
}
|
||||
|
||||
fun addImageRequestListener(listener: ImageRequest.Listener) {
|
||||
val list = listeners ?: LinkedList<ImageRequest.Listener>().also { listeners = it }
|
||||
list.add(listener)
|
||||
}
|
||||
|
||||
fun removeImageRequestListener(listener: ImageRequest.Listener) {
|
||||
listeners?.remove(listener)
|
||||
}
|
||||
|
||||
fun setImageAsync(@DrawableRes resourceId: Int) = enqueueRequest(
|
||||
newRequestBuilder()
|
||||
.data(resourceId)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun setImageAsync(url: String?) = enqueueRequest(
|
||||
newRequestBuilder()
|
||||
.data(url)
|
||||
.build(),
|
||||
)
|
||||
|
||||
@Deprecated("Use more specific overrides instead")
|
||||
fun setImageAsync(request: ImageRequest) = enqueueRequest(
|
||||
request.newBuilder()
|
||||
.lifecycle(request.lifecycle ?: findViewTreeLifecycleOwner()?.lifecycle)
|
||||
.target(this)
|
||||
.size(
|
||||
if (request.sizeResolver == SizeResolver.ORIGINAL) {
|
||||
ViewSizeResolver(this)
|
||||
} else {
|
||||
request.sizeResolver
|
||||
},
|
||||
).build(),
|
||||
)
|
||||
|
||||
fun disposeImage() {
|
||||
CoilUtils.dispose(this)
|
||||
currentRequest = null
|
||||
currentImageData = NullRequestData
|
||||
setImageDrawable(null)
|
||||
}
|
||||
|
||||
protected fun enqueueRequest(request: ImageRequest): Disposable {
|
||||
val previous = currentRequest
|
||||
if (currentImageData == request.data && previous?.job?.isCancelled == false) {
|
||||
return previous
|
||||
}
|
||||
currentImageData = request.data
|
||||
return coil.enqueue(request).also { currentRequest = it }
|
||||
}
|
||||
|
||||
protected open fun newRequestBuilder() = ImageRequest.Builder(context).apply {
|
||||
lifecycle(findViewTreeLifecycleOwner())
|
||||
val crossfadeDuration = if (context.isAnimationsEnabled) {
|
||||
(context.getAnimationDuration(R.integer.config_defaultAnimTime) * crossfadeDurationFactor).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
crossfade(crossfadeDuration)
|
||||
if (useExistingDrawable) {
|
||||
val previousDrawable = this@CoilImageView.drawable?.asImage()
|
||||
if (previousDrawable != null) {
|
||||
fallback(previousDrawable)
|
||||
placeholder(previousDrawable)
|
||||
error(previousDrawable)
|
||||
} else {
|
||||
setupPlaceholders()
|
||||
}
|
||||
} else {
|
||||
setupPlaceholders()
|
||||
}
|
||||
if (decodeRegion) {
|
||||
decodeRegion(0)
|
||||
}
|
||||
size(
|
||||
exactImageSize?.let {
|
||||
SizeResolver(it)
|
||||
} ?: ViewSizeResolver(this@CoilImageView),
|
||||
)
|
||||
scale(scaleType.toCoilScale())
|
||||
listener(this@CoilImageView)
|
||||
allowRgb565(allowRgb565)
|
||||
target(this@CoilImageView)
|
||||
}
|
||||
|
||||
private fun ImageRequest.Builder.setupPlaceholders() {
|
||||
placeholder(placeholderDrawable?.asImage())
|
||||
error(errorDrawable?.asImage())
|
||||
fallback(fallbackDrawable?.asImage())
|
||||
}
|
||||
|
||||
private fun ScaleType.toCoilScale(): Scale = if (this == ScaleType.CENTER_CROP) {
|
||||
Scale.FILL
|
||||
} else {
|
||||
Scale.FIT
|
||||
}
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.ViewTreeObserver.OnPreDrawListener
|
||||
import android.widget.ImageView
|
||||
import coil3.size.Dimension
|
||||
import coil3.size.Size
|
||||
import coil3.size.ViewSizeResolver
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val ASPECT_RATIO_HEIGHT = 18f
|
||||
private const val ASPECT_RATIO_WIDTH = 13f
|
||||
|
||||
class CoverSizeResolver(
|
||||
override val view: ImageView,
|
||||
) : ViewSizeResolver<ImageView> {
|
||||
|
||||
override suspend fun size(): Size {
|
||||
// Fast path: the view is already measured.
|
||||
getSize()?.let { return it }
|
||||
|
||||
// Slow path: wait for the view to be measured.
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val viewTreeObserver = view.viewTreeObserver
|
||||
|
||||
val preDrawListener = object : OnPreDrawListener {
|
||||
private var isResumed = false
|
||||
|
||||
override fun onPreDraw(): Boolean {
|
||||
val size = getSize()
|
||||
if (size != null) {
|
||||
viewTreeObserver.removePreDrawListenerSafe(this)
|
||||
|
||||
if (!isResumed) {
|
||||
isResumed = true
|
||||
continuation.resume(size)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
viewTreeObserver.addOnPreDrawListener(preDrawListener)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
viewTreeObserver.removePreDrawListenerSafe(preDrawListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSize(): Size? {
|
||||
var width = getWidth()
|
||||
var height = getHeight()
|
||||
when {
|
||||
width == null && height == null -> {
|
||||
return null
|
||||
}
|
||||
height == null && width != null -> {
|
||||
height = Dimension((width.px * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt())
|
||||
}
|
||||
width == null && height != null -> {
|
||||
width = Dimension((height.px * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt())
|
||||
}
|
||||
}
|
||||
return Size(checkNotNull(width), checkNotNull(height))
|
||||
}
|
||||
|
||||
private fun getWidth() = getDimension(
|
||||
paramSize = view.layoutParams?.width ?: -1,
|
||||
viewSize = view.width,
|
||||
paddingSize = if (subtractPadding) view.paddingLeft + view.paddingRight else 0
|
||||
)
|
||||
|
||||
private fun getHeight() = getDimension(
|
||||
paramSize = view.layoutParams?.height ?: -1,
|
||||
viewSize = view.height,
|
||||
paddingSize = if (subtractPadding) view.paddingTop + view.paddingBottom else 0
|
||||
)
|
||||
|
||||
private fun getDimension(paramSize: Int, viewSize: Int, paddingSize: Int): Dimension.Pixels? {
|
||||
if (paramSize == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||
return null
|
||||
}
|
||||
val insetParamSize = paramSize - paddingSize
|
||||
if (insetParamSize > 0) {
|
||||
return Dimension(insetParamSize)
|
||||
}
|
||||
val insetViewSize = viewSize - paddingSize
|
||||
if (insetViewSize > 0) {
|
||||
return Dimension(insetViewSize)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun ViewTreeObserver.removePreDrawListenerSafe(victim: OnPreDrawListener) {
|
||||
if (isAlive) {
|
||||
removeOnPreDrawListener(victim)
|
||||
} else {
|
||||
view.viewTreeObserver.removeOnPreDrawListener(victim)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package org.koitharu.kotatsu.core.ui.image
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import coil3.Image
|
||||
import coil3.asImage
|
||||
import coil3.request.Disposable
|
||||
import coil3.request.ImageRequest
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier.Companion.ignoreCaptchaErrors
|
||||
import org.koitharu.kotatsu.core.image.CoilImageView
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class FaviconView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : CoilImageView(context, attrs, defStyleAttr) {
|
||||
|
||||
@StyleRes
|
||||
private var iconStyle: Int = R.style.FaviconDrawable
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.FaviconView, defStyleAttr) {
|
||||
iconStyle = getResourceId(R.styleable.FaviconView_iconStyle, iconStyle)
|
||||
}
|
||||
if (isInEditMode) {
|
||||
setImageDrawable(
|
||||
FaviconDrawable(
|
||||
context = context,
|
||||
styleResId = iconStyle,
|
||||
name = context.getString(R.string.app_name).random().toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setImageAsync(mangaSource: MangaSource): Disposable {
|
||||
val fallbackFactory: (ImageRequest) -> Image? = {
|
||||
FaviconDrawable(context, iconStyle, mangaSource.name).asImage()
|
||||
}
|
||||
val placeholderFactory: (ImageRequest) -> Image? = if (context.isAnimationsEnabled) {
|
||||
{ AnimatedFaviconDrawable(context, iconStyle, mangaSource.name).asImage() }
|
||||
} else {
|
||||
fallbackFactory
|
||||
}
|
||||
return enqueueRequest(
|
||||
newRequestBuilder()
|
||||
.data(mangaSource.faviconUri())
|
||||
.error(fallbackFactory)
|
||||
.fallback(fallbackFactory)
|
||||
.placeholder(placeholderFactory)
|
||||
.mangaSourceExtra(mangaSource)
|
||||
.ignoreCaptchaErrors()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout.HORIZONTAL
|
||||
import android.widget.LinearLayout.VERTICAL
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import org.koitharu.kotatsu.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val ASPECT_RATIO_HEIGHT = 3f
|
||||
private const val ASPECT_RATIO_WIDTH = 2f
|
||||
|
||||
class CoverImageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : ShapeableImageView(context, attrs, defStyleAttr) {
|
||||
|
||||
private var orientation: Int = HORIZONTAL
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) {
|
||||
orientation = getInt(R.styleable.CoverImageView_android_orientation, orientation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
val desiredWidth: Int
|
||||
val desiredHeight: Int
|
||||
if (orientation == VERTICAL) {
|
||||
desiredHeight = measuredHeight
|
||||
desiredWidth = (desiredHeight * ASPECT_RATIO_WIDTH / ASPECT_RATIO_HEIGHT).roundToInt()
|
||||
} else {
|
||||
desiredWidth = measuredWidth
|
||||
desiredHeight = (desiredWidth * ASPECT_RATIO_HEIGHT / ASPECT_RATIO_WIDTH).roundToInt()
|
||||
}
|
||||
setMeasuredDimension(desiredWidth, desiredHeight)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package org.koitharu.kotatsu.core.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isEmpty
|
||||
import androidx.core.view.isGone
|
||||
|
||||
open class StackLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : ViewGroup(context, attrs, defStyleAttr) {
|
||||
|
||||
private val visibleChildren = ArrayList<View>()
|
||||
|
||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||
val w = r - l - paddingLeft - paddingRight
|
||||
val h = b - t - paddingTop - paddingBottom
|
||||
visibleChildren.clear()
|
||||
children.filterNotTo(visibleChildren) { it.isGone }
|
||||
if (w <= 0 || h <= 0 || visibleChildren.isEmpty) {
|
||||
return
|
||||
}
|
||||
val xStep = w / (visibleChildren.size + 1)
|
||||
val yStep = h / (visibleChildren.size + 1)
|
||||
val maxW = w
|
||||
val maxH = h
|
||||
val total = visibleChildren.size
|
||||
for ((index, child) in visibleChildren.withIndex()) {
|
||||
var cx = paddingLeft + xStep * (total - index)
|
||||
var cy = paddingTop + yStep * (index + 1)
|
||||
val rx = child.measuredWidth.coerceAtMost(maxW) / 2
|
||||
val ry = child.measuredHeight.coerceAtMost(maxH) / 2
|
||||
if (cx < rx) {
|
||||
cx = rx
|
||||
}
|
||||
if (cy < ry) {
|
||||
cy = ry
|
||||
}
|
||||
if (cx + rx > width) {
|
||||
cx = width - rx
|
||||
}
|
||||
if (cy + ry > height) {
|
||||
cy = height - ry
|
||||
}
|
||||
child.layout(cx - rx, cy - ry, cx + rx, cy + ry)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
measureChildren(widthMeasureSpec, heightMeasureSpec)
|
||||
if (isEmpty()) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
return
|
||||
}
|
||||
|
||||
var h = 0
|
||||
var w = 0
|
||||
for (i in 0 until childCount) {
|
||||
val child = getChildAt(i)
|
||||
if (child.isGone) {
|
||||
continue
|
||||
}
|
||||
val mw = child.measuredWidth
|
||||
val mh = child.measuredHeight
|
||||
if (h == 0 || w == 0) {
|
||||
h = mh
|
||||
w = mw
|
||||
} else {
|
||||
h += mh / 2
|
||||
w += mw / 2
|
||||
}
|
||||
}
|
||||
h += paddingTop + paddingBottom
|
||||
w += paddingLeft + paddingRight
|
||||
setMeasuredDimension(
|
||||
resolveSizeAndState(w, widthMeasureSpec, 0),
|
||||
resolveSizeAndState(h, heightMeasureSpec, 0),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,243 @@
|
||||
package org.koitharu.kotatsu.image.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.ViewTreeObserver.OnPreDrawListener
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil3.request.transformations
|
||||
import coil3.size.Dimension
|
||||
import coil3.size.Size
|
||||
import coil3.size.ViewSizeResolver
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.image.CoilImageView
|
||||
import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable
|
||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||
import org.koitharu.kotatsu.core.util.ext.bookmarkExtra
|
||||
import org.koitharu.kotatsu.core.util.ext.decodeRegion
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaExtra
|
||||
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import kotlin.coroutines.resume
|
||||
import androidx.appcompat.R as appcompatR
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class CoverImageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = R.attr.coverImageViewStyle,
|
||||
) : CoilImageView(context, attrs, defStyleAttr) {
|
||||
|
||||
private var aspectRationHeight: Int = 0
|
||||
private var aspectRationWidth: Int = 0
|
||||
var trimImage: Boolean = false
|
||||
|
||||
private val hasAspectRatio: Boolean
|
||||
get() = aspectRationHeight > 0 && aspectRationWidth > 0
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.CoverImageView, defStyleAttr) {
|
||||
aspectRationHeight = getInt(R.styleable.CoverImageView_aspectRationHeight, aspectRationHeight)
|
||||
aspectRationWidth = getInt(R.styleable.CoverImageView_aspectRationWidth, aspectRationWidth)
|
||||
trimImage = getBoolean(R.styleable.CoverImageView_trimImage, trimImage)
|
||||
}
|
||||
if (placeholderDrawable == null) {
|
||||
placeholderDrawable = AnimatedPlaceholderDrawable(context)
|
||||
}
|
||||
if (errorDrawable == null) {
|
||||
errorDrawable = ColorUtils.blendARGB(
|
||||
context.getThemeColor(materialR.attr.colorErrorContainer),
|
||||
context.getThemeColor(appcompatR.attr.colorBackgroundFloating),
|
||||
0.25f,
|
||||
).toDrawable()
|
||||
}
|
||||
if (fallbackDrawable == null) {
|
||||
fallbackDrawable = context.getThemeColor(materialR.attr.colorSurfaceContainer).toDrawable()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
if (!hasAspectRatio) {
|
||||
return
|
||||
}
|
||||
val isExactWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
|
||||
val isExactHeight = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY
|
||||
when {
|
||||
isExactHeight && isExactWidth -> Unit
|
||||
isExactHeight -> setMeasuredDimension(
|
||||
/* measuredWidth = */ measuredHeight * aspectRationWidth / aspectRationHeight,
|
||||
/* measuredHeight = */ measuredHeight,
|
||||
)
|
||||
|
||||
isExactWidth -> setMeasuredDimension(
|
||||
/* measuredWidth = */ measuredWidth,
|
||||
/* measuredHeight = */ measuredWidth * aspectRationHeight / aspectRationWidth,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setImageAsync(page: ReaderPage) = enqueueRequest(
|
||||
newRequestBuilder()
|
||||
.data(page.preview?.nullIfEmpty() ?: page.toMangaPage())
|
||||
.mangaSourceExtra(page.source)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun setImageAsync(page: MangaPage) = enqueueRequest(
|
||||
newRequestBuilder()
|
||||
.data(page.preview?.nullIfEmpty() ?: page)
|
||||
.mangaSourceExtra(page.source)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun setImageAsync(cover: Cover?) = enqueueRequest(
|
||||
newRequestBuilder()
|
||||
.data(cover?.url)
|
||||
.mangaSourceExtra(cover?.mangaSource)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun setImageAsync(
|
||||
coverUrl: String?,
|
||||
manga: Manga?,
|
||||
) = enqueueRequest(
|
||||
newRequestBuilder()
|
||||
.data(coverUrl)
|
||||
.mangaExtra(manga)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun setImageAsync(
|
||||
coverUrl: String?,
|
||||
source: MangaSource,
|
||||
) = enqueueRequest(
|
||||
newRequestBuilder()
|
||||
.data(coverUrl)
|
||||
.mangaSourceExtra(source)
|
||||
.build(),
|
||||
)
|
||||
|
||||
fun setImageAsync(
|
||||
bookmark: Bookmark
|
||||
) = enqueueRequest(
|
||||
newRequestBuilder()
|
||||
.data(bookmark.imageLoadData)
|
||||
.decodeRegion(bookmark.scroll)
|
||||
.bookmarkExtra(bookmark)
|
||||
.build(),
|
||||
)
|
||||
|
||||
override fun newRequestBuilder() = super.newRequestBuilder().apply {
|
||||
if (trimImage) {
|
||||
transformations(listOf(TrimTransformation()))
|
||||
}
|
||||
if (hasAspectRatio) {
|
||||
size(CoverSizeResolver(this@CoverImageView))
|
||||
}
|
||||
}
|
||||
|
||||
private class CoverSizeResolver(
|
||||
override val view: CoverImageView,
|
||||
) : ViewSizeResolver<CoverImageView> {
|
||||
|
||||
override suspend fun size(): Size {
|
||||
// Fast path: the view is already measured.
|
||||
getSize()?.let { return it }
|
||||
|
||||
// Slow path: wait for the view to be measured.
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val viewTreeObserver = view.viewTreeObserver
|
||||
|
||||
val preDrawListener = object : OnPreDrawListener {
|
||||
private var isResumed = false
|
||||
|
||||
override fun onPreDraw(): Boolean {
|
||||
val size = getSize()
|
||||
if (size != null) {
|
||||
viewTreeObserver.removePreDrawListenerSafe(this)
|
||||
|
||||
if (!isResumed) {
|
||||
isResumed = true
|
||||
continuation.resume(size)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
viewTreeObserver.addOnPreDrawListener(preDrawListener)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
viewTreeObserver.removePreDrawListenerSafe(preDrawListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSize(): Size? {
|
||||
var width = getWidth()
|
||||
var height = getHeight()
|
||||
when {
|
||||
width == null && height == null -> {
|
||||
return null
|
||||
}
|
||||
|
||||
height == null -> {
|
||||
height = Dimension(width!!.px * view.aspectRationHeight / view.aspectRationWidth)
|
||||
}
|
||||
|
||||
width == null -> {
|
||||
width = Dimension(height.px * view.aspectRationWidth / view.aspectRationHeight)
|
||||
}
|
||||
}
|
||||
return Size(checkNotNull(width), checkNotNull(height))
|
||||
}
|
||||
|
||||
private fun getWidth() = getDimension(
|
||||
paramSize = view.layoutParams?.width ?: -1,
|
||||
viewSize = view.width,
|
||||
paddingSize = if (subtractPadding) view.paddingLeft + view.paddingRight else 0,
|
||||
)
|
||||
|
||||
private fun getHeight() = getDimension(
|
||||
paramSize = view.layoutParams?.height ?: -1,
|
||||
viewSize = view.height,
|
||||
paddingSize = if (subtractPadding) view.paddingTop + view.paddingBottom else 0,
|
||||
)
|
||||
|
||||
private fun getDimension(paramSize: Int, viewSize: Int, paddingSize: Int): Dimension.Pixels? {
|
||||
if (paramSize == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||
return null
|
||||
}
|
||||
val insetParamSize = paramSize - paddingSize
|
||||
if (insetParamSize > 0) {
|
||||
return Dimension(insetParamSize)
|
||||
}
|
||||
val insetViewSize = viewSize - paddingSize
|
||||
if (insetViewSize > 0) {
|
||||
return Dimension(insetViewSize)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun ViewTreeObserver.removePreDrawListenerSafe(victim: OnPreDrawListener) {
|
||||
if (isAlive) {
|
||||
removeOnPreDrawListener(victim)
|
||||
} else {
|
||||
view.viewTreeObserver.removeOnPreDrawListener(victim)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package org.koitharu.kotatsu.image.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.Px
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.UnknownMangaSource
|
||||
import org.koitharu.kotatsu.core.ui.widgets.StackLayout
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.databinding.ViewCoverStackBinding
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class CoverStackView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : StackLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val binding = ViewCoverStackBinding.inflate(LayoutInflater.from(context), this)
|
||||
private val coverViews = arrayOf(
|
||||
binding.imageViewCover1,
|
||||
binding.imageViewCover2,
|
||||
binding.imageViewCover3,
|
||||
)
|
||||
private var hideEmptyView: Boolean = true
|
||||
|
||||
init {
|
||||
context.withStyledAttributes(attrs, R.styleable.CoverStackView, defStyleAttr) {
|
||||
val coverSize = getDimension(R.styleable.CoverStackView_coverSize, 0f)
|
||||
if (coverSize > 0f) {
|
||||
setCoverSize(coverSize)
|
||||
}
|
||||
}
|
||||
val backgroundColor = context.getThemeColor(android.R.attr.colorBackground)
|
||||
ImageViewCompat.setImageTintList(
|
||||
binding.imageViewCover3,
|
||||
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153)),
|
||||
)
|
||||
ImageViewCompat.setImageTintList(
|
||||
binding.imageViewCover2,
|
||||
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76)),
|
||||
)
|
||||
binding.imageViewCover2.backgroundTintList = ColorStateList.valueOf(
|
||||
ColorUtils.setAlphaComponent(backgroundColor, 76),
|
||||
)
|
||||
binding.imageViewCover3.backgroundTintList = ColorStateList.valueOf(
|
||||
ColorUtils.setAlphaComponent(backgroundColor, 153),
|
||||
)
|
||||
coverViews.forEachIndexed { index, view ->
|
||||
view.crossfadeDurationFactor = index + 1f
|
||||
}
|
||||
}
|
||||
|
||||
fun setCoversAsync(covers: List<Cover>) {
|
||||
coverViews.forEachIndexed { index, view ->
|
||||
view.setImageAsync(covers.getOrNull(index))
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("setMangaCoversAsync")
|
||||
fun setCoversAsync(manga: List<Manga>) {
|
||||
coverViews.forEachIndexed { index, view ->
|
||||
val m = manga.getOrNull(index)
|
||||
view.setCoverOrHide(m?.coverUrl, m, m?.source)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCoverSize(@Px coverSize: Float) {
|
||||
val coverWidth = (coverSize * 13f).toInt()
|
||||
val coverHeight = (coverSize * 18f).toInt()
|
||||
children.forEach {
|
||||
it.updateLayoutParams {
|
||||
width = coverWidth
|
||||
height = coverHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoverImageView.setCoverOrHide(url: String?, manga: Manga?, source: MangaSource?) {
|
||||
if (url.isNullOrEmpty() && hideEmptyView) {
|
||||
disposeImage()
|
||||
isVisible = false
|
||||
} else {
|
||||
isVisible = true
|
||||
if (manga != null) {
|
||||
setImageAsync(url, manga)
|
||||
} else {
|
||||
setImageAsync(url, source ?: UnknownMangaSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.colorfilter
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import coil3.target.ImageViewTarget
|
||||
|
||||
class DoubleViewTarget(
|
||||
primaryView: ImageView,
|
||||
private val secondaryView: ImageView,
|
||||
) : ImageViewTarget(primaryView) {
|
||||
|
||||
override var drawable: Drawable?
|
||||
get() = super.drawable
|
||||
set(value) {
|
||||
super.drawable = value
|
||||
secondaryView.setImageDrawable(value?.constantState?.newDrawable())
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/Widget.Material3.CardView.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/bookmark_item_height"
|
||||
app:cardCornerRadius="12dp">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.CoverImageView
|
||||
android:id="@+id/imageView_thumb"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/ic_placeholder" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue