refactor(ReaderProgressBar): Use compose

This commit is contained in:
Ahmad Ansori Palembani 2024-06-13 07:19:00 +07:00
parent a2a36ca839
commit fef5723b0a
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
4 changed files with 177 additions and 211 deletions

View file

@ -0,0 +1,125 @@
package dev.yokai.presentation.component
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.tooling.preview.Preview
/**
* A combined [CircularProgressIndicator] that always rotates.
*
* By always rotating we give the feedback to the user that the application isn't 'stuck'.
*/
@Composable
fun CombinedCircularProgressIndicator(
progress: () -> Float,
isInverted: () -> Boolean,
) {
AnimatedContent(
targetState = progress() == 0f,
transitionSpec = { fadeIn() togetherWith fadeOut() },
label = "progressState",
) { indeterminate ->
if (indeterminate) {
// Indeterminate
CircularProgressIndicator()
} else {
// Determinate
val infiniteTransition = rememberInfiniteTransition(label = "infiniteRotation")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "rotation",
)
val animatedProgress by animateFloatAsState(
targetValue = progress(),
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
label = "progress",
)
CircularProgressIndicator(
progress = { animatedProgress },
modifier = Modifier.rotate(rotation),
color = if (isInverted()) MaterialTheme.colorScheme.inversePrimary else MaterialTheme.colorScheme.primary
)
}
}
}
@Preview
@Composable
private fun CombinedCircularProgressIndicatorPreview() {
var progress by remember { mutableFloatStateOf(0f) }
var isInverted by remember { mutableStateOf(false) }
MaterialTheme {
Scaffold(
bottomBar = {
Column(
modifier = Modifier.fillMaxWidth(),
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
progress = when (progress) {
0f -> 0.15f
0.15f -> 0.25f
0.25f -> 0.5f
0.5f -> 0.75f
0.75f -> 0.95f
else -> 0f
}
},
) {
Text("Progress")
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { isInverted = !isInverted },
) {
Text("Invert")
}
}
},
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.padding(it),
) {
CombinedCircularProgressIndicator(progress = { progress }, isInverted = { isInverted })
}
}
}
}

View file

@ -3,21 +3,22 @@ package eu.kanade.tachiyomi.ui.reader.viewer
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.view.animation.Animation
import android.view.Gravity
import android.view.animation.DecelerateInterpolator
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlin.math.min
import android.widget.FrameLayout
import androidx.annotation.IntRange
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.view.isVisible
import dev.yokai.presentation.component.CombinedCircularProgressIndicator
import dev.yokai.presentation.theme.YokaiTheme
/**
* A custom progress bar that always rotates while being determinate. By always rotating we give
@ -28,148 +29,39 @@ class ReaderProgressBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr) {
) : AbstractComposeView(context, attrs, defStyleAttr) {
/**
* The current sweep angle. It always starts at 10% because otherwise the bar and the rotation
* wouldn't be visible.
*/
private var sweepAngle = 10f
private var progress by mutableFloatStateOf(0f)
private var isInvertedFromTheme by mutableStateOf(false)
/**
* Whether the parent views are also visible.
*/
private var aggregatedIsVisible = false
/**
* The paint to use to draw the progress bar.
*/
private var paint = setPaint()
override fun setForegroundTintList(tint: ColorStateList?) {
super.setForegroundTintList(tint)
paint = setPaint()
fun setInvertMode(value: Boolean) {
isInvertedFromTheme = value
}
private fun setPaint(): Paint {
return Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = foregroundTintList?.defaultColor ?: context.getResourceColor(R.attr.colorSecondary)
isAntiAlias = true
strokeCap = Paint.Cap.ROUND
style = Paint.Style.STROKE
init {
layoutParams = FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER)
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool)
}
@Composable
override fun Content() {
YokaiTheme {
CombinedCircularProgressIndicator(progress = { progress }, isInverted = { isInvertedFromTheme })
}
}
/**
* The rectangle of the canvas where the progress bar should be drawn. This is calculated on
* layout.
*/
private val ovalRect = RectF()
/**
* The rotation animation to use while the progress bar is visible.
*/
private val rotationAnimation by lazy {
RotateAnimation(
0f,
360f,
Animation.RELATIVE_TO_SELF,
0.5f,
Animation.RELATIVE_TO_SELF,
0.5f,
).apply {
interpolator = LinearInterpolator()
repeatCount = Animation.INFINITE
duration = 4000
}
}
/**
* Called when the view is layout. The position and thickness of the progress bar is calculated.
*/
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val diameter = min(width, height)
val thickness = diameter / 10f
val pad = thickness / 2f
ovalRect.set(pad, pad, diameter - pad, diameter - pad)
paint.strokeWidth = thickness
}
/**
* Called when the view is being drawn. An arc is drawn with the calculated rectangle. The
* animation will take care of rotation.
*/
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawArc(ovalRect, -90f, sweepAngle, false, paint)
}
/**
* Calculates the sweep angle to use from the progress.
*/
private fun calcSweepAngleFromProgress(progress: Int): Float {
return 360f / 100 * progress
}
/**
* Called when this view is attached to window. It starts the rotation animation.
*/
override fun onAttachedToWindow() {
super.onAttachedToWindow()
startAnimation()
}
/**
* Called when this view is detached to window. It stops the rotation animation.
*/
override fun onDetachedFromWindow() {
stopAnimation()
super.onDetachedFromWindow()
}
/**
* Called when the visibility of this view changes.
*/
override fun setVisibility(visibility: Int) {
super.setVisibility(visibility)
val isVisible = visibility == View.VISIBLE
if (isVisible) {
startAnimation()
} else {
stopAnimation()
}
}
/**
* Starts the rotation animation if needed.
*/
private fun startAnimation() {
if (visibility != View.VISIBLE || windowVisibility != View.VISIBLE || animation != null) {
return
}
animation = rotationAnimation
animation.start()
}
/**
* Stops the rotation animation if needed.
*/
private fun stopAnimation() {
clearAnimation()
fun show() {
isVisible = true
}
/**
* Hides this progress bar with an optional fade out if [animate] is true.
*/
fun hide(animate: Boolean = false) {
if (visibility == View.GONE) return
if (!isVisible) return
if (!animate) {
visibility = View.GONE
isVisible = false
} else {
ObjectAnimator.ofFloat(this, "alpha", 1f, 0f).apply {
interpolator = DecelerateInterpolator()
@ -177,7 +69,7 @@ class ReaderProgressBar @JvmOverloads constructor(
addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
visibility = View.GONE
isVisible = false
alpha = 1f
}
@ -195,34 +87,11 @@ class ReaderProgressBar @JvmOverloads constructor(
* Completes this progress bar and fades out the view.
*/
fun completeAndFadeOut() {
setRealProgress(100)
setProgress(100)
hide(true)
}
/**
* Set progress of the circular progress bar ensuring a min max range in order to notice the
* rotation animation.
*/
fun setProgress(progress: Int) {
// Scale progress in [10, 95] range
val scaledProgress = 85 * progress / 100 + 10
setRealProgress(scaledProgress)
}
/**
* Sets the real progress of the circular progress bar. Note that if this progres is 0 or
* 100, the rotation animation won't be noticed by the user because nothing changes in the
* canvas.
*/
private fun setRealProgress(progress: Int) {
ValueAnimator.ofFloat(sweepAngle, calcSweepAngleFromProgress(progress)).apply {
interpolator = DecelerateInterpolator()
duration = 250
addUpdateListener { valueAnimator ->
sweepAngle = valueAnimator.animatedValue as Float
invalidate()
}
start()
}
fun setProgress(@IntRange(from = 0, to = 100) progress: Int) {
this.progress = progress / 100f
}
}

View file

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
@ -11,13 +10,11 @@ import android.graphics.RectF
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.view.Gravity
import android.view.LayoutInflater
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import co.touchlab.kermit.Logger
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
import eu.kanade.tachiyomi.source.model.Page
@ -32,8 +29,6 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.ImageUtil.isPagePadded
import eu.kanade.tachiyomi.util.system.ThemeUtil
import eu.kanade.tachiyomi.util.system.bottomCutoutInset
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.isInNightMode
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.launchUI
@ -75,7 +70,7 @@ class PagerPageHolder(
/**
* Loading progress bar to indicate the current progress.
*/
private val progressBar = createProgressBar()
private val progressBar = ReaderProgressBar(context)
/**
* Error layout to show when the image fails to load.
@ -139,15 +134,7 @@ class PagerPageHolder(
else -> ThemeUtil.readerBackgroundColor(theme)
},
)
progressBar.foregroundTintList = ColorStateList.valueOf(
context.getResourceColor(
if (isInvertedFromTheme()) {
R.attr.colorPrimaryInverse
} else {
R.attr.colorPrimary
},
),
)
progressBar.setInvertMode(isInvertedFromTheme())
}
override fun onImageLoaded() {
@ -447,7 +434,7 @@ class PagerPageHolder(
* Called when the page is queued.
*/
private fun setQueued() {
progressBar.isVisible = true
progressBar.show()
errorLayout?.isVisible = false
}
@ -455,7 +442,7 @@ class PagerPageHolder(
* Called when the page is loading.
*/
private fun setLoading() {
progressBar.isVisible = true
progressBar.show()
errorLayout?.isVisible = false
}
@ -463,7 +450,7 @@ class PagerPageHolder(
* Called when the page is downloading.
*/
private fun setDownloading() {
progressBar.isVisible = true
progressBar.show()
errorLayout?.isVisible = false
}
@ -471,7 +458,7 @@ class PagerPageHolder(
* Called when the page is ready.
*/
private fun setImage() {
progressBar.isVisible = true
progressBar.show()
if (extraPage == null) {
progressBar.completeAndFadeOut()
} else {
@ -572,7 +559,7 @@ class PagerPageHolder(
* Called when the page has an error.
*/
private fun setError() {
progressBar.isVisible = false
progressBar.hide()
showErrorLayout(false)
}
@ -580,29 +567,17 @@ class PagerPageHolder(
* Called when the image is decoded and going to be displayed.
*/
private fun onImageDecoded() {
progressBar.isVisible = false
progressBar.hide()
}
/**
* Called when an image fails to decode.
*/
private fun onImageDecodeError() {
progressBar.isVisible = false
progressBar.hide()
showErrorLayout(true)
}
/**
* Creates a new progress bar.
*/
private fun createProgressBar(): ReaderProgressBar {
return ReaderProgressBar(context, null).apply {
val size = 48.dpToPx
layoutParams = LayoutParams(size, size).apply {
gravity = Gravity.CENTER
}
}
}
private fun isInvertedFromTheme(): Boolean {
return when (backgroundColor) {
Color.WHITE -> context.isInNightMode()

View file

@ -1,15 +1,15 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.annotation.SuppressLint
import android.content.res.Resources
import android.graphics.Color
import android.view.Gravity
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.core.view.updatePaddingRelative
import co.touchlab.kermit.Logger
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
@ -199,7 +199,7 @@ class WebtoonPageHolder(
*/
private fun setQueued() {
progressContainer.isVisible = true
progressBar.isVisible = true
progressBar.show()
removeErrorLayout()
}
@ -208,7 +208,7 @@ class WebtoonPageHolder(
*/
private fun setLoading() {
progressContainer.isVisible = true
progressBar.isVisible = true
progressBar.show()
removeErrorLayout()
}
@ -217,7 +217,7 @@ class WebtoonPageHolder(
*/
private fun setDownloading() {
progressContainer.isVisible = true
progressBar.isVisible = true
progressBar.show()
removeErrorLayout()
}
@ -226,7 +226,7 @@ class WebtoonPageHolder(
*/
private suspend fun setImage() {
progressContainer.isVisible = true
progressBar.isVisible = true
progressBar.show()
progressBar.completeAndFadeOut()
removeErrorLayout()
@ -297,16 +297,13 @@ class WebtoonPageHolder(
/**
* Creates a new progress bar.
*/
@SuppressLint("PrivateResource")
private fun createProgressBar(): ReaderProgressBar {
progressContainer = FrameLayout(context)
frame.addView(progressContainer, MATCH_PARENT, parentHeight)
val progress = ReaderProgressBar(context).apply {
val size = 48.dpToPx
layoutParams = FrameLayout.LayoutParams(size, size).apply {
gravity = Gravity.CENTER_HORIZONTAL
setMargins(0, parentHeight / 4, 0, 0)
updateLayoutParams<FrameLayout.LayoutParams> {
updateMargins(top = parentHeight / 4)
}
}
progressContainer.addView(progress)