From 6ba779fb7ae014d1f6e23d46cabf8c633d279d8e Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Wed, 21 Jul 2021 04:38:19 +0700 Subject: [PATCH] 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 --- .../ui/reader/viewer/ReaderProgressBar.kt | 215 ------------------ .../reader/viewer/ReaderProgressIndicator.kt | 77 +++++++ .../ui/reader/viewer/pager/PagerPageHolder.kt | 42 ++-- .../viewer/pager/PagerTransitionHolder.kt | 5 +- .../viewer/webtoon/WebtoonPageHolder.kt | 31 ++- .../viewer/webtoon/WebtoonTransitionHolder.kt | 5 +- app/src/main/res/layout/reader_activity.xml | 1 - 7 files changed, 115 insertions(+), 261 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressBar.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt 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 deleted file mode 100644 index 3afc4da31a..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressBar.kt +++ /dev/null @@ -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() - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt new file mode 100644 index 0000000000..0858818636 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt @@ -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) + } +} 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 22955f3b60..455c4308f6 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 @@ -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 { + 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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt index 5c3d556f4e..86f9329e39 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt @@ -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() 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 9c6886357b..d5470ed97f 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,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 { gravity = Gravity.CENTER_HORIZONTAL - setMargins(0, parentHeight / 4, 0, 0) + updateMargins(top = parentHeight / 4) } } progressContainer.addView(progress) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt index 16f08654c8..d14f5bc0a8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt @@ -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() diff --git a/app/src/main/res/layout/reader_activity.xml b/app/src/main/res/layout/reader_activity.xml index 0de593cd26..fb90ddaf5e 100644 --- a/app/src/main/res/layout/reader_activity.xml +++ b/app/src/main/res/layout/reader_activity.xml @@ -22,7 +22,6 @@ android:layout_gravity="center" android:indeterminate="true" android:visibility="gone" - app:indicatorSize="56dp" tools:visibility="visible" />