mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
refactor(ReaderProgressBar): Use compose
This commit is contained in:
parent
a2a36ca839
commit
fef5723b0a
4 changed files with 177 additions and 211 deletions
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue