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:
parent
8bd965267c
commit
6ba779fb7a
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user