Reader loading progress indicator changes (#5587)

* Use CircularProgressIndicator on PageHolder

Manually rotate the CircularProgressIndicator inside a wrapper view instead of
drawing our own custom indicator.

* Use CircularProgressIndicator on TransitionHolder
This commit is contained in:
Ivan Iskandar 2021-07-21 04:38:19 +07:00 committed by GitHub
parent 8bd965267c
commit 6ba779fb7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 115 additions and 261 deletions

View file

@ -1,215 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
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.animation.DecelerateInterpolator
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import androidx.core.animation.doOnCancel
import androidx.core.animation.doOnEnd
import androidx.core.view.isGone
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlin.math.min
/**
* A custom progress bar that always rotates while being determinate. By always rotating we give
* the feedback to the user that the application isn't 'stuck', and by making it determinate the
* user also approximately knows how much the operation will take.
*/
class ReaderProgressBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(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
/**
* Whether the parent views are also visible.
*/
private var aggregatedIsVisible = false
/**
* The paint to use to draw the progress bar.
*/
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getResourceColor(R.attr.colorAccent)
isAntiAlias = true
strokeCap = Paint.Cap.ROUND
style = Paint.Style.STROKE
}
/**
* 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 == VISIBLE
if (isVisible) {
startAnimation()
} else {
stopAnimation()
}
}
/**
* Starts the rotation animation if needed.
*/
private fun startAnimation() {
if (visibility != VISIBLE || windowVisibility != VISIBLE || animation != null) {
return
}
animation = rotationAnimation
animation.start()
}
/**
* Stops the rotation animation if needed.
*/
private fun stopAnimation() {
clearAnimation()
}
/**
* Hides this progress bar with an optional fade out if [animate] is true.
*/
fun hide(animate: Boolean = false) {
if (isGone) return
if (!animate) {
isVisible = false
} else {
ObjectAnimator.ofFloat(this, "alpha", 1f, 0f).apply {
interpolator = DecelerateInterpolator()
duration = 1000
doOnEnd {
isVisible = false
alpha = 1f
}
doOnCancel {
alpha = 1f
}
start()
}
}
}
/**
* Completes this progress bar and fades out the view.
*/
fun completeAndFadeOut() {
setRealProgress(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()
}
}
}

View file

@ -0,0 +1,77 @@
package eu.kanade.tachiyomi.ui.reader.viewer
import android.content.Context
import android.util.AttributeSet
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 androidx.annotation.IntRange
import androidx.core.view.isVisible
import com.google.android.material.progressindicator.CircularProgressIndicator
/**
* A wrapper for [CircularProgressIndicator] that always rotates while being determinate.
*
* By always rotating we give the feedback to the user that the application isn't 'stuck',
* and by making it determinate the user also approximately knows how much the operation will take.
*/
class ReaderProgressIndicator @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(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 {
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
indicator = CircularProgressIndicator(context)
indicator.max = 100
addView(indicator)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (indicator.isVisible && animation == null) {
startAnimation(rotateAnimation)
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
clearAnimation()
}
fun show() {
indicator.show()
if (animation == null) {
startAnimation(rotateAnimation)
}
}
fun hide() {
indicator.hide()
clearAnimation()
}
fun setProgress(@IntRange(from = 0, to = 100) progress: Int, animated: Boolean = true) {
indicator.setProgressCompat(progress, animated)
}
}

View file

@ -14,6 +14,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
@ -24,7 +25,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.ImageUtil
@ -56,7 +57,11 @@ class PagerPageHolder(
/**
* Loading progress bar to indicate the current progress.
*/
private val progressBar = createProgressBar()
private val progressIndicator = ReaderProgressIndicator(context).apply {
updateLayoutParams<LayoutParams> {
gravity = Gravity.CENTER
}
}
/**
* Image view that supports subsampling on zoom.
@ -95,7 +100,7 @@ class PagerPageHolder(
private var readImageHeaderSubscription: Subscription? = null
init {
addView(progressBar)
addView(progressIndicator)
observeStatus()
}
@ -136,7 +141,7 @@ class PagerPageHolder(
.distinctUntilChanged()
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { value -> progressBar.setProgress(value) }
.subscribe { value -> progressIndicator.setProgress(value) }
}
/**
@ -191,7 +196,7 @@ class PagerPageHolder(
* Called when the page is queued.
*/
private fun setQueued() {
progressBar.isVisible = true
progressIndicator.show()
retryButton?.isVisible = false
decodeErrorLayout?.isVisible = false
}
@ -200,7 +205,7 @@ class PagerPageHolder(
* Called when the page is loading.
*/
private fun setLoading() {
progressBar.isVisible = true
progressIndicator.show()
retryButton?.isVisible = false
decodeErrorLayout?.isVisible = false
}
@ -209,7 +214,7 @@ class PagerPageHolder(
* Called when the page is downloading.
*/
private fun setDownloading() {
progressBar.isVisible = true
progressIndicator.show()
retryButton?.isVisible = false
decodeErrorLayout?.isVisible = false
}
@ -218,8 +223,8 @@ class PagerPageHolder(
* Called when the page is ready.
*/
private fun setImage() {
progressBar.isVisible = true
progressBar.completeAndFadeOut()
progressIndicator.setProgress(100)
progressIndicator.hide()
retryButton?.isVisible = false
decodeErrorLayout?.isVisible = false
@ -301,7 +306,7 @@ class PagerPageHolder(
* Called when the page has an error.
*/
private fun setError() {
progressBar.isVisible = false
progressIndicator.hide()
initRetryButton().isVisible = true
}
@ -309,30 +314,17 @@ class PagerPageHolder(
* Called when the image is decoded and going to be displayed.
*/
private fun onImageDecoded() {
progressBar.isVisible = false
progressIndicator.hide()
}
/**
* Called when an image fails to decode.
*/
private fun onImageDecodeError() {
progressBar.isVisible = false
progressIndicator.hide()
initDecodeErrorLayout().isVisible = true
}
/**
* Creates a new progress bar.
*/
@SuppressLint("PrivateResource")
private fun createProgressBar(): ReaderProgressBar {
return ReaderProgressBar(context, null).apply {
val size = 48.dpToPx
layoutParams = LayoutParams(size, size).apply {
gravity = Gravity.CENTER
}
}
}
/**
* Initializes a subsampling scale view.
*/

View file

@ -7,8 +7,8 @@ import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import android.widget.ProgressBar
import androidx.appcompat.widget.AppCompatTextView
import com.google.android.material.progressindicator.CircularProgressIndicator
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
@ -96,7 +96,8 @@ class PagerTransitionHolder(
* Sets the loading state on the pages container.
*/
private fun setLoading() {
val progress = ProgressBar(context, null, android.R.attr.progressBarStyle)
val progress = CircularProgressIndicator(context)
progress.isIndeterminate = true
val textView = AppCompatTextView(context).apply {
wrapContent()

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.annotation.SuppressLint
import android.content.res.Resources
import android.graphics.drawable.Animatable
import android.view.Gravity
@ -14,6 +13,8 @@ import android.widget.TextView
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import coil.clear
import coil.imageLoader
import coil.request.CachePolicy
@ -23,7 +24,7 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.dpToPx
@ -50,7 +51,7 @@ class WebtoonPageHolder(
/**
* Loading progress bar to indicate the current progress.
*/
private val progressBar = createProgressBar()
private val progressIndicator = createProgressIndicator()
/**
* Progress bar container. Needed to keep a minimum height size of the holder, otherwise the
@ -144,7 +145,7 @@ class WebtoonPageHolder(
subsamplingImageView?.isVisible = false
imageView?.clear()
imageView?.isVisible = false
progressBar.setProgress(0)
progressIndicator.setProgress(0, animated = false)
}
/**
@ -177,7 +178,7 @@ class WebtoonPageHolder(
.distinctUntilChanged()
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { value -> progressBar.setProgress(value) }
.subscribe { value -> progressIndicator.setProgress(value) }
addSubscription(progressSubscription)
}
@ -235,7 +236,7 @@ class WebtoonPageHolder(
*/
private fun setQueued() {
progressContainer.isVisible = true
progressBar.isVisible = true
progressIndicator.show()
retryContainer?.isVisible = false
removeDecodeErrorLayout()
}
@ -245,7 +246,7 @@ class WebtoonPageHolder(
*/
private fun setLoading() {
progressContainer.isVisible = true
progressBar.isVisible = true
progressIndicator.show()
retryContainer?.isVisible = false
removeDecodeErrorLayout()
}
@ -255,7 +256,7 @@ class WebtoonPageHolder(
*/
private fun setDownloading() {
progressContainer.isVisible = true
progressBar.isVisible = true
progressIndicator.show()
retryContainer?.isVisible = false
removeDecodeErrorLayout()
}
@ -265,8 +266,8 @@ class WebtoonPageHolder(
*/
private fun setImage() {
progressContainer.isVisible = true
progressBar.isVisible = true
progressBar.completeAndFadeOut()
progressIndicator.setProgress(100)
progressIndicator.hide()
retryContainer?.isVisible = false
removeDecodeErrorLayout()
@ -342,16 +343,14 @@ class WebtoonPageHolder(
/**
* Creates a new progress bar.
*/
@SuppressLint("PrivateResource")
private fun createProgressBar(): ReaderProgressBar {
private fun createProgressIndicator(): ReaderProgressIndicator {
progressContainer = FrameLayout(context)
frame.addView(progressContainer, MATCH_PARENT, parentHeight)
val progress = ReaderProgressBar(context).apply {
val size = 48.dpToPx
layoutParams = FrameLayout.LayoutParams(size, size).apply {
val progress = ReaderProgressIndicator(context).apply {
updateLayoutParams<FrameLayout.LayoutParams> {
gravity = Gravity.CENTER_HORIZONTAL
setMargins(0, parentHeight / 4, 0, 0)
updateMargins(top = parentHeight / 4)
}
}
progressContainer.addView(progress)

View file

@ -4,11 +4,11 @@ import android.view.Gravity
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import android.widget.ProgressBar
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.isNotEmpty
import androidx.core.view.isVisible
import com.google.android.material.progressindicator.CircularProgressIndicator
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
@ -111,7 +111,8 @@ class WebtoonTransitionHolder(
* Sets the loading state on the pages container.
*/
private fun setLoading() {
val progress = ProgressBar(context, null, android.R.attr.progressBarStyle)
val progress = CircularProgressIndicator(context)
progress.isIndeterminate = true
val textView = AppCompatTextView(context).apply {
wrapContent()

View file

@ -22,7 +22,6 @@
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
app:indicatorSize="56dp"
tools:visibility="visible" />
<eu.kanade.tachiyomi.ui.reader.PageIndicatorTextView