Show reading progress indicators in lists
parent
b82b46f7d7
commit
04dd8003f7
@ -0,0 +1,129 @@
|
|||||||
|
package org.koitharu.kotatsu.history.ui.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.*
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class ReadingProgressDrawable(
|
||||||
|
context: Context,
|
||||||
|
@StyleRes styleResId: Int,
|
||||||
|
) : Drawable() {
|
||||||
|
|
||||||
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||||
|
private val lineColor: Int
|
||||||
|
private val outlineColor: Int
|
||||||
|
private val backgroundColor: Int
|
||||||
|
private val textColor: Int
|
||||||
|
private val textPattern = context.getString(R.string.percent_string_pattern)
|
||||||
|
private val textBounds = Rect()
|
||||||
|
private val hasBackground: Boolean
|
||||||
|
private val hasOutline: Boolean
|
||||||
|
private val hasText: Boolean
|
||||||
|
private val desiredHeight: Int
|
||||||
|
private val desiredWidth: Int
|
||||||
|
private val autoFitTextSize: Boolean
|
||||||
|
|
||||||
|
var progress: Float = PROGRESS_NONE
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
text = textPattern.format((value * 100f).roundToInt().toString())
|
||||||
|
paint.getTextBounds(text, 0, text.length, textBounds)
|
||||||
|
invalidateSelf()
|
||||||
|
}
|
||||||
|
private var text = ""
|
||||||
|
|
||||||
|
init {
|
||||||
|
val ta = context.obtainStyledAttributes(styleResId, R.styleable.ProgressDrawable)
|
||||||
|
desiredHeight = ta.getDimensionPixelSize(R.styleable.ProgressDrawable_android_height, -1)
|
||||||
|
desiredWidth = ta.getDimensionPixelSize(R.styleable.ProgressDrawable_android_width, -1)
|
||||||
|
autoFitTextSize = ta.getBoolean(R.styleable.ProgressDrawable_autoFitTextSize, false)
|
||||||
|
lineColor = ta.getColor(R.styleable.ProgressDrawable_android_strokeColor, Color.BLACK)
|
||||||
|
outlineColor = ta.getColor(R.styleable.ProgressDrawable_outlineColor, Color.TRANSPARENT)
|
||||||
|
backgroundColor = ColorUtils.setAlphaComponent(
|
||||||
|
ta.getColor(R.styleable.ProgressDrawable_android_fillColor, Color.TRANSPARENT),
|
||||||
|
(255 * ta.getFloat(R.styleable.ProgressDrawable_android_fillAlpha, 0f)).toInt(),
|
||||||
|
)
|
||||||
|
textColor = ta.getColor(R.styleable.ProgressDrawable_android_textColor, lineColor)
|
||||||
|
paint.strokeCap = Paint.Cap.ROUND
|
||||||
|
paint.textAlign = Paint.Align.CENTER
|
||||||
|
paint.textSize = ta.getDimension(R.styleable.ProgressDrawable_android_textSize, paint.textSize)
|
||||||
|
paint.strokeWidth = ta.getDimension(R.styleable.ProgressDrawable_strokeWidth, 1f)
|
||||||
|
hasBackground = Color.alpha(backgroundColor) != 0
|
||||||
|
hasOutline = Color.alpha(outlineColor) != 0
|
||||||
|
hasText = Color.alpha(textColor) != 0 && paint.textSize > 0
|
||||||
|
ta.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBoundsChange(bounds: Rect) {
|
||||||
|
super.onBoundsChange(bounds)
|
||||||
|
if (autoFitTextSize) {
|
||||||
|
val innerWidth = bounds.width() - (paint.strokeWidth * 2f)
|
||||||
|
paint.textSize = getTextSizeForWidth(innerWidth, "100%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
if (progress < 0f) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val cx = bounds.exactCenterX()
|
||||||
|
val cy = bounds.exactCenterY()
|
||||||
|
val radius = minOf(bounds.width(), bounds.height()) / 2f
|
||||||
|
if (hasBackground) {
|
||||||
|
paint.style = Paint.Style.FILL
|
||||||
|
paint.color = backgroundColor
|
||||||
|
canvas.drawCircle(cx, cy, radius, paint)
|
||||||
|
}
|
||||||
|
val innerRadius = radius - paint.strokeWidth / 2f
|
||||||
|
paint.style = Paint.Style.STROKE
|
||||||
|
if (hasOutline) {
|
||||||
|
paint.color = outlineColor
|
||||||
|
canvas.drawCircle(cx, cy, innerRadius, paint)
|
||||||
|
}
|
||||||
|
paint.color = lineColor
|
||||||
|
canvas.drawArc(
|
||||||
|
cx - innerRadius,
|
||||||
|
cy - innerRadius,
|
||||||
|
cx + innerRadius,
|
||||||
|
cy + innerRadius,
|
||||||
|
-90f,
|
||||||
|
360f * progress,
|
||||||
|
false,
|
||||||
|
paint,
|
||||||
|
)
|
||||||
|
if (hasText) {
|
||||||
|
paint.style = Paint.Style.FILL
|
||||||
|
paint.color = textColor
|
||||||
|
val ty = bounds.height() / 2f + textBounds.height() / 2f - textBounds.bottom
|
||||||
|
canvas.drawText(text, cx, ty, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAlpha(alpha: Int) {
|
||||||
|
paint.alpha = alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||||
|
paint.colorFilter = colorFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DeprecatedCallableAddReplaceWith")
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
override fun getOpacity() = PixelFormat.TRANSLUCENT
|
||||||
|
|
||||||
|
override fun getIntrinsicHeight() = desiredHeight
|
||||||
|
|
||||||
|
override fun getIntrinsicWidth() = desiredWidth
|
||||||
|
|
||||||
|
private fun getTextSizeForWidth(width: Float, text: String): Float {
|
||||||
|
val testTextSize = 48f
|
||||||
|
paint.textSize = testTextSize
|
||||||
|
paint.getTextBounds(text, 0, text.length, textBounds)
|
||||||
|
return testTextSize * width / textBounds.width()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
package org.koitharu.kotatsu.history.ui.util
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Outline
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewOutlineProvider
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||||
|
|
||||||
|
class ReadingProgressView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
@AttrRes defStyleAttr: Int = 0,
|
||||||
|
) : View(context, attrs, defStyleAttr), ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {
|
||||||
|
|
||||||
|
private var percentAnimator: ValueAnimator? = null
|
||||||
|
private val animationDuration = context.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
|
||||||
|
|
||||||
|
@StyleRes
|
||||||
|
private val drawableStyle: Int
|
||||||
|
|
||||||
|
var percent: Float
|
||||||
|
get() = peekProgressDrawable()?.progress ?: PROGRESS_NONE
|
||||||
|
set(value) {
|
||||||
|
cancelAnimation()
|
||||||
|
getProgressDrawable().progress = value
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val ta = context.obtainStyledAttributes(attrs, R.styleable.ReadingProgressView, defStyleAttr, 0)
|
||||||
|
drawableStyle = ta.getResourceId(R.styleable.ReadingProgressView_progressStyle, R.style.ProgressDrawable)
|
||||||
|
ta.recycle()
|
||||||
|
outlineProvider = OutlineProvider()
|
||||||
|
if (isInEditMode) {
|
||||||
|
percent = 0.27f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
percentAnimator?.run {
|
||||||
|
if (isRunning) end()
|
||||||
|
}
|
||||||
|
percentAnimator = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationUpdate(animation: ValueAnimator) {
|
||||||
|
val p = animation.animatedValue as Float
|
||||||
|
getProgressDrawable().progress = p
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationStart(animation: Animator?) = Unit
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
if (percentAnimator === animation) {
|
||||||
|
percentAnimator = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animation: Animator?) = Unit
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(animation: Animator?) = Unit
|
||||||
|
|
||||||
|
fun setPercent(value: Float, animate: Boolean) {
|
||||||
|
val currentDrawable = peekProgressDrawable()
|
||||||
|
if (!animate || currentDrawable == null || value == PROGRESS_NONE) {
|
||||||
|
percent = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
percentAnimator?.cancel()
|
||||||
|
percentAnimator = ValueAnimator.ofFloat(
|
||||||
|
currentDrawable.progress.coerceAtLeast(0f),
|
||||||
|
value
|
||||||
|
).apply {
|
||||||
|
duration = animationDuration
|
||||||
|
interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
addUpdateListener(this@ReadingProgressView)
|
||||||
|
addListener(this@ReadingProgressView)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelAnimation() {
|
||||||
|
percentAnimator?.cancel()
|
||||||
|
percentAnimator = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun peekProgressDrawable(): ReadingProgressDrawable? {
|
||||||
|
return background as? ReadingProgressDrawable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProgressDrawable(): ReadingProgressDrawable {
|
||||||
|
var d = peekProgressDrawable()
|
||||||
|
if (d != null) {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
d = ReadingProgressDrawable(context, drawableStyle)
|
||||||
|
background = d
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OutlineProvider : ViewOutlineProvider() {
|
||||||
|
|
||||||
|
override fun getOutline(view: View, outline: Outline) {
|
||||||
|
outline.setOval(0, 0, view.width, view.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.list.domain
|
package org.koitharu.kotatsu.list.domain
|
||||||
|
|
||||||
fun interface CountersProvider {
|
interface ListExtraProvider {
|
||||||
|
|
||||||
suspend fun getCounter(mangaId: Long): Int
|
suspend fun getCounter(mangaId: Long): Int
|
||||||
|
|
||||||
|
suspend fun getProgress(mangaId: Long): Float
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue