ReaderProgressIndicator: Convert to Compose (#9574)
This commit is contained in:
parent
0d96791a84
commit
39e4568460
5 changed files with 137 additions and 81 deletions
|
@ -15,7 +15,6 @@ import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
@ -24,7 +23,6 @@ import android.view.View.LAYER_TYPE_HARDWARE
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
@ -36,7 +34,6 @@ import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
import com.google.android.material.internal.ToolbarUtils
|
import com.google.android.material.internal.ToolbarUtils
|
||||||
|
@ -69,7 +66,6 @@ import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.preference.toggle
|
import eu.kanade.tachiyomi.util.preference.toggle
|
||||||
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||||
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
|
|
||||||
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import eu.kanade.tachiyomi.util.system.isNightMode
|
import eu.kanade.tachiyomi.util.system.isNightMode
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
|
@ -660,12 +656,7 @@ class ReaderActivity : BaseActivity() {
|
||||||
|
|
||||||
supportActionBar?.title = manga.title
|
supportActionBar?.title = manga.title
|
||||||
|
|
||||||
val loadingIndicatorContext = createReaderThemeContext()
|
loadingIndicator = ReaderProgressIndicator(this)
|
||||||
loadingIndicator = ReaderProgressIndicator(loadingIndicatorContext).apply {
|
|
||||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
|
||||||
gravity = Gravity.CENTER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.readerContainer.addView(loadingIndicator)
|
binding.readerContainer.addView(loadingIndicator)
|
||||||
|
|
||||||
startPostponedEnterTransition()
|
startPostponedEnterTransition()
|
||||||
|
|
|
@ -2,13 +2,21 @@ package eu.kanade.tachiyomi.ui.reader.viewer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.LinearInterpolator
|
|
||||||
import android.view.animation.RotateAnimation
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.annotation.IntRange
|
import androidx.annotation.IntRange
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.platform.AbstractComposeView
|
||||||
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
|
import tachiyomi.presentation.core.components.CombinedCircularProgressIndicator
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper for [CircularProgressIndicator] that always rotates.
|
* A wrapper for [CircularProgressIndicator] that always rotates.
|
||||||
|
@ -19,76 +27,31 @@ class ReaderProgressIndicator @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0,
|
defStyleAttr: Int = 0,
|
||||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
) : AbstractComposeView(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private val indicator: CircularProgressIndicator
|
|
||||||
|
|
||||||
private val rotateAnimation 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
|
layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, Gravity.CENTER)
|
||||||
indicator = CircularProgressIndicator(context)
|
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool)
|
||||||
indicator.max = 100
|
|
||||||
indicator.isIndeterminate = true
|
|
||||||
addView(indicator)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
private var progress by mutableFloatStateOf(0f)
|
||||||
super.onAttachedToWindow()
|
|
||||||
updateRotateAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
@Composable
|
||||||
super.onDetachedFromWindow()
|
override fun Content() {
|
||||||
updateRotateAnimation()
|
TachiyomiTheme {
|
||||||
|
CombinedCircularProgressIndicator(progress = progress)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show() {
|
fun show() {
|
||||||
indicator.show()
|
isVisible = true
|
||||||
updateRotateAnimation()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hide() {
|
fun hide() {
|
||||||
indicator.hide()
|
isVisible = false
|
||||||
updateRotateAnimation()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun setProgress(@IntRange(from = 0, to = 100) progress: Int) {
|
||||||
* Sets the current indicator progress to the specified value.
|
this.progress = progress / 100f
|
||||||
*
|
|
||||||
* @param progress Indicator will be set indeterminate if this value is 0
|
|
||||||
*/
|
|
||||||
fun setProgress(@IntRange(from = 0, to = 100) progress: Int, animated: Boolean = true) {
|
|
||||||
if (progress > 0) {
|
|
||||||
indicator.setProgressCompat(progress, animated)
|
|
||||||
} else if (!indicator.isIndeterminate) {
|
|
||||||
indicator.hide()
|
|
||||||
indicator.isIndeterminate = true
|
|
||||||
indicator.show()
|
|
||||||
}
|
|
||||||
updateRotateAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateRotateAnimation() {
|
|
||||||
if (isAttachedToWindow && indicator.isShown && !indicator.isIndeterminate) {
|
|
||||||
if (animation == null) {
|
|
||||||
startAnimation(rotateAnimation)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
clearAnimation()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,8 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
import eu.kanade.tachiyomi.databinding.ReaderErrorBinding
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
|
||||||
|
@ -46,11 +44,7 @@ class PagerPageHolder(
|
||||||
/**
|
/**
|
||||||
* Loading progress bar to indicate the current progress.
|
* Loading progress bar to indicate the current progress.
|
||||||
*/
|
*/
|
||||||
private val progressIndicator: ReaderProgressIndicator = ReaderProgressIndicator(readerThemedContext).apply {
|
private val progressIndicator: ReaderProgressIndicator = ReaderProgressIndicator(readerThemedContext)
|
||||||
updateLayoutParams<LayoutParams> {
|
|
||||||
gravity = Gravity.CENTER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error layout to show when the image fails to load.
|
* Error layout to show when the image fails to load.
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
@ -119,7 +118,7 @@ class WebtoonPageHolder(
|
||||||
|
|
||||||
removeErrorLayout()
|
removeErrorLayout()
|
||||||
frame.recycle()
|
frame.recycle()
|
||||||
progressIndicator.setProgress(0, animated = false)
|
progressIndicator.setProgress(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -288,7 +287,6 @@ class WebtoonPageHolder(
|
||||||
|
|
||||||
val progress = ReaderProgressIndicator(context).apply {
|
val progress = ReaderProgressIndicator(context).apply {
|
||||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||||
gravity = Gravity.CENTER_HORIZONTAL
|
|
||||||
updateMargins(top = parentHeight / 4)
|
updateMargins(top = parentHeight / 4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
package tachiyomi.presentation.core.components
|
||||||
|
|
||||||
|
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.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.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,
|
||||||
|
) {
|
||||||
|
val animatedProgress by animateFloatAsState(
|
||||||
|
targetValue = progress,
|
||||||
|
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||||
|
label = "progress",
|
||||||
|
)
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = animatedProgress,
|
||||||
|
modifier = Modifier.rotate(rotation),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun CombinedCircularProgressIndicatorPreview() {
|
||||||
|
var progress by remember { mutableFloatStateOf(0f) }
|
||||||
|
MaterialTheme {
|
||||||
|
Scaffold(
|
||||||
|
bottomBar = {
|
||||||
|
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("change")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(it),
|
||||||
|
) {
|
||||||
|
CombinedCircularProgressIndicator(progress = progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in a new issue