From fef5723b0aadef86818494ef6f5d916516c74684 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Thu, 13 Jun 2024 07:19:00 +0700 Subject: [PATCH] refactor(ReaderProgressBar): Use compose --- .../component/CircularProgressIndicator.kt | 125 +++++++++++ .../ui/reader/viewer/ReaderProgressBar.kt | 201 +++--------------- .../ui/reader/viewer/pager/PagerPageHolder.kt | 43 +--- .../viewer/webtoon/WebtoonPageHolder.kt | 19 +- 4 files changed, 177 insertions(+), 211 deletions(-) create mode 100644 app/src/main/java/dev/yokai/presentation/component/CircularProgressIndicator.kt diff --git a/app/src/main/java/dev/yokai/presentation/component/CircularProgressIndicator.kt b/app/src/main/java/dev/yokai/presentation/component/CircularProgressIndicator.kt new file mode 100644 index 0000000000..6f7f36d130 --- /dev/null +++ b/app/src/main/java/dev/yokai/presentation/component/CircularProgressIndicator.kt @@ -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 }) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressBar.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressBar.kt index bb3886f5fa..9bfc417f2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressBar.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressBar.kt @@ -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 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index e1ae53304e..dd59015d8a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 600c3666da..8936e61c88 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -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 { + updateMargins(top = parentHeight / 4) } } progressContainer.addView(progress)