Webtoon reader now shows download progress. Keep the progress bar until the image is decoded

This commit is contained in:
len 2016-10-23 18:59:25 +02:00
parent 22bbcaeed0
commit 90e0e0b72a
7 changed files with 209 additions and 162 deletions

View file

@ -29,7 +29,7 @@ class PageDecodeErrorLayout(context: Context) : LinearLayout(context) {
init { init {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
setGravity(Gravity.CENTER) gravity = Gravity.CENTER
} }
constructor(context: Context, page: Page, theme: Int, retryListener: () -> Unit) : this(context) { constructor(context: Context, page: Page, theme: Int, retryListener: () -> Unit) : this(context) {
@ -47,7 +47,6 @@ class PageDecodeErrorLayout(context: Context) : LinearLayout(context) {
layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
setText(R.string.action_retry) setText(R.string.action_retry)
setOnClickListener { setOnClickListener {
removeAllViews()
retryListener() retryListener()
} }
addView(this) addView(this)

View file

@ -12,9 +12,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader.Companion.ALIGN_CENTER
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader.Companion.ALIGN_LEFT
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader.Companion.ALIGN_RIGHT
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader
import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader
import kotlinx.android.synthetic.main.chapter_image.view.* import kotlinx.android.synthetic.main.chapter_image.view.*
@ -33,8 +30,12 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
/** /**
* Page of a chapter. * Page of a chapter.
*/ */
var page: Page? = null lateinit var page: Page
private set
/**
* Subscription for status changes of the page.
*/
private var statusSubscription: Subscription? = null
/** /**
* Subscription for progress changes of the page. * Subscription for progress changes of the page.
@ -42,11 +43,11 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
private var progressSubscription: Subscription? = null private var progressSubscription: Subscription? = null
/** /**
* Subscription for status changes of the page. * Layout of decode error.
*/ */
private var statusSubscription: Subscription? = null private var decodeErrorLayout: PageDecodeErrorLayout? = null
fun initialize(reader: PagerReader, page: Page?) { fun initialize(reader: PagerReader, page: Page) {
val activity = reader.activity as ReaderActivity val activity = reader.activity as ReaderActivity
when (activity.readerTheme) { when (activity.readerTheme) {
@ -71,19 +72,11 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
setOnTouchListener { v, motionEvent -> reader.gestureDetector.onTouchEvent(motionEvent) } setOnTouchListener { v, motionEvent -> reader.gestureDetector.onTouchEvent(motionEvent) }
setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() { setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
override fun onReady() { override fun onReady() {
when (reader.zoomType) { onImageDecoded(reader)
ALIGN_LEFT -> setScaleAndCenter(scale, PointF(0f, 0f))
ALIGN_RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f))
ALIGN_CENTER -> {
val newCenter = center
newCenter.y = 0f
setScaleAndCenter(scale, newCenter)
}
}
} }
override fun onImageLoadError(e: Exception) { override fun onImageLoadError(e: Exception) {
onImageDecodeError(activity) onImageDecodeError(reader)
} }
}) })
} }
@ -95,21 +88,15 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
true true
} }
if (page != null) {
this.page = page this.page = page
observeStatus() observeStatus()
} }
}
fun cleanup() { override fun onDetachedFromWindow() {
unsubscribeProgress() unsubscribeProgress()
unsubscribeStatus() unsubscribeStatus()
image_view.setOnTouchListener(null) image_view.setOnTouchListener(null)
image_view.setOnImageEventListener(null) image_view.setOnImageEventListener(null)
}
override fun onDetachedFromWindow() {
cleanup()
super.onDetachedFromWindow() super.onDetachedFromWindow()
} }
@ -120,7 +107,6 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
*/ */
private fun observeStatus() { private fun observeStatus() {
statusSubscription?.unsubscribe() statusSubscription?.unsubscribe()
val page = page ?: return
val statusSubject = SerializedSubject(PublishSubject.create<Int>()) val statusSubject = SerializedSubject(PublishSubject.create<Int>())
page.setStatusSubject(statusSubject) page.setStatusSubject(statusSubject)
@ -135,7 +121,6 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
*/ */
private fun observeProgress() { private fun observeProgress() {
progressSubscription?.unsubscribe() progressSubscription?.unsubscribe()
val page = page ?: return
progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS) progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS)
.map { page.progress } .map { page.progress }
@ -154,18 +139,18 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
*/ */
private fun processStatus(status: Int) { private fun processStatus(status: Int) {
when (status) { when (status) {
Page.QUEUE -> hideError() Page.QUEUE -> setQueued()
Page.LOAD_PAGE -> onLoading() Page.LOAD_PAGE -> setLoading()
Page.DOWNLOAD_IMAGE -> { Page.DOWNLOAD_IMAGE -> {
observeProgress() observeProgress()
onDownloading() setDownloading()
} }
Page.READY -> { Page.READY -> {
onReady() setImage()
unsubscribeProgress() unsubscribeProgress()
} }
Page.ERROR -> { Page.ERROR -> {
onError() setError()
unsubscribeProgress() unsubscribeProgress()
} }
} }
@ -175,7 +160,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
* Unsubscribes from the status subscription. * Unsubscribes from the status subscription.
*/ */
private fun unsubscribeStatus() { private fun unsubscribeStatus() {
page?.setStatusSubject(null) page.setStatusSubject(null)
statusSubscription?.unsubscribe() statusSubscription?.unsubscribe()
statusSubscription = null statusSubscription = null
} }
@ -188,10 +173,23 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
progressSubscription = null progressSubscription = null
} }
/**
* Called when the page is queued.
*/
private fun setQueued() {
progress_container.visibility = View.VISIBLE
progress_text.visibility = View.INVISIBLE
retry_button.visibility = View.GONE
decodeErrorLayout?.let {
removeView(it)
decodeErrorLayout = null
}
}
/** /**
* Called when the page is loading. * Called when the page is loading.
*/ */
private fun onLoading() { private fun setLoading() {
progress_container.visibility = View.VISIBLE progress_container.visibility = View.VISIBLE
progress_text.visibility = View.VISIBLE progress_text.visibility = View.VISIBLE
progress_text.setText(R.string.downloading) progress_text.setText(R.string.downloading)
@ -200,7 +198,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
/** /**
* Called when the page is downloading. * Called when the page is downloading.
*/ */
private fun onDownloading() { private fun setDownloading() {
progress_container.visibility = View.VISIBLE progress_container.visibility = View.VISIBLE
progress_text.visibility = View.VISIBLE progress_text.visibility = View.VISIBLE
} }
@ -208,42 +206,51 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
/** /**
* Called when the page is ready. * Called when the page is ready.
*/ */
private fun onReady() { private fun setImage() {
page?.imagePath?.let { path -> val path = page.imagePath
if (File(path).exists()) { if (path != null && File(path).exists()) {
progress_text.visibility = View.INVISIBLE
image_view.setImage(ImageSource.uri(path)) image_view.setImage(ImageSource.uri(path))
progress_container.visibility = View.GONE
} else { } else {
page?.status = Page.ERROR page.status = Page.ERROR
}
} }
} }
/** /**
* Called when the page has an error. * Called when the page has an error.
*/ */
private fun onError() { private fun setError() {
progress_container.visibility = View.GONE progress_container.visibility = View.GONE
retry_button.visibility = View.VISIBLE retry_button.visibility = View.VISIBLE
} }
/** /**
* Hides the error layout. * Called when the image is decoded and going to be displayed.
*/ */
private fun hideError() { private fun onImageDecoded(reader: PagerReader) {
retry_button.visibility = View.GONE progress_container.visibility = View.GONE
with(image_view) {
when (reader.zoomType) {
PagerReader.ALIGN_LEFT -> setScaleAndCenter(scale, PointF(0f, 0f))
PagerReader.ALIGN_RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f))
PagerReader.ALIGN_CENTER -> setScaleAndCenter(scale, center.apply { y = 0f })
}
}
} }
/** /**
* Called when an image fails to decode. * Called when an image fails to decode.
*/ */
private fun onImageDecodeError(activity: ReaderActivity) { private fun onImageDecodeError(reader: PagerReader) {
page?.let { page -> if (decodeErrorLayout != null || !reader.isAdded) return
val errorLayout = PageDecodeErrorLayout(context, page, activity.readerTheme,
val activity = reader.activity as ReaderActivity
decodeErrorLayout = PageDecodeErrorLayout(context, page, activity.readerTheme,
{ activity.presenter.retryPage(page) }) { activity.presenter.retryPage(page) })
addView(errorLayout) addView(decodeErrorLayout)
}
} }
} }

View file

@ -15,7 +15,7 @@ class PagerReaderAdapter(private val reader: PagerReader) : ViewPagerAdapter() {
/** /**
* Pages stored in the adapter. * Pages stored in the adapter.
*/ */
var pages: List<Page>? = null var pages: List<Page> = emptyList()
set(value) { set(value) {
field = value field = value
notifyDataSetChanged() notifyDataSetChanged()
@ -23,17 +23,15 @@ class PagerReaderAdapter(private val reader: PagerReader) : ViewPagerAdapter() {
override fun createView(container: ViewGroup, position: Int): View { override fun createView(container: ViewGroup, position: Int): View {
val view = container.inflate(R.layout.item_pager_reader) as PageView val view = container.inflate(R.layout.item_pager_reader) as PageView
view.initialize(reader, pages?.getOrNull(position)) view.initialize(reader, pages[position])
return view return view
} }
/** /**
* Returns the number of pages. * Returns the number of pages.
*
* @return the number of pages or 0 if the list is null.
*/ */
override fun getCount(): Int { override fun getCount(): Int {
return pages?.size ?: 0 return pages.size
} }
} }

View file

@ -72,7 +72,7 @@ class WebtoonAdapter(val fragment: WebtoonReader) : RecyclerView.Adapter<Webtoon
* @param holder the holder to recycle. * @param holder the holder to recycle.
*/ */
override fun onViewRecycled(holder: WebtoonHolder) { override fun onViewRecycled(holder: WebtoonHolder) {
holder.unsubscribeStatus() holder.onRecycle()
} }
} }

View file

@ -6,16 +6,19 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.source.model.Page import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
import kotlinx.android.synthetic.main.chapter_image.view.* import kotlinx.android.synthetic.main.chapter_image.view.*
import kotlinx.android.synthetic.main.item_webtoon_reader.view.* import kotlinx.android.synthetic.main.item_webtoon_reader.view.*
import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import rx.subjects.SerializedSubject import rx.subjects.SerializedSubject
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
/** /**
* Holder for webtoon reader for a single page of a chapter. * Holder for webtoon reader for a single page of a chapter.
@ -38,6 +41,11 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
*/ */
private var statusSubscription: Subscription? = null private var statusSubscription: Subscription? = null
/**
* Subscription for progress changes of the page.
*/
private var progressSubscription: Subscription? = null
/** /**
* Layout of decode error. * Layout of decode error.
*/ */
@ -57,8 +65,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
setOnTouchListener(adapter.touchListener) setOnTouchListener(adapter.touchListener)
setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() { setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
override fun onImageLoaded() { override fun onImageLoaded() {
// When the image is loaded, reset the minimum height to avoid gaps onImageDecoded()
view.frame_container.minimumHeight = 30
} }
override fun onImageLoadError(e: Exception) { override fun onImageLoadError(e: Exception) {
@ -67,16 +74,9 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
}) })
} }
// Avoid to create a lot of view holders taking twice the screen height, view.progress_container.minimumHeight = view.resources.displayMetrics.heightPixels
// saving memory and a possible OOM. When the first image is loaded in this holder,
// the minimum size will be removed.
// Doing this we get sequential holder instantiation.
view.frame_container.minimumHeight = view.resources.displayMetrics.heightPixels * 2
// Leave some space between progress bars view.setOnTouchListener(adapter.touchListener)
view.progress.minimumHeight = 300
view.frame_container.setOnTouchListener(adapter.touchListener)
view.retry_button.setOnTouchListener { v, event -> view.retry_button.setOnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_UP) { if (event.action == MotionEvent.ACTION_UP) {
readerActivity.presenter.retryPage(page) readerActivity.presenter.retryPage(page)
@ -92,13 +92,22 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
* @param page the page to bind. * @param page the page to bind.
*/ */
fun onSetValues(page: Page) { fun onSetValues(page: Page) {
this.page = page
observeStatus()
}
/**
* Called when the view is recycled and added to the view pool.
*/
fun onRecycle() {
unsubscribeStatus()
unsubscribeProgress()
decodeErrorLayout?.let { decodeErrorLayout?.let {
(view as ViewGroup).removeView(it) (view as ViewGroup).removeView(it)
decodeErrorLayout = null decodeErrorLayout = null
} }
view.image_view.recycle()
this.page = page view.progress_container.visibility = View.VISIBLE
observeStatus()
} }
/** /**
@ -107,7 +116,8 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
* @see processStatus * @see processStatus
*/ */
private fun observeStatus() { private fun observeStatus() {
page?.let { page -> val page = page ?: return
val statusSubject = SerializedSubject(PublishSubject.create<Int>()) val statusSubject = SerializedSubject(PublishSubject.create<Int>())
page.setStatusSubject(statusSubject) page.setStatusSubject(statusSubject)
@ -118,6 +128,23 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
webtoonReader.subscriptions.add(statusSubscription) webtoonReader.subscriptions.add(statusSubscription)
} }
/**
* Observes the progress of the page and updates view.
*/
private fun observeProgress() {
progressSubscription?.unsubscribe()
val page = page ?: return
progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS)
.map { page.progress }
.distinctUntilChanged()
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { progress ->
view.progress_text.text = view.context.getString(R.string.download_progress, progress)
}
} }
/** /**
@ -127,105 +154,110 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
*/ */
private fun processStatus(status: Int) { private fun processStatus(status: Int) {
when (status) { when (status) {
Page.QUEUE -> onQueue() Page.QUEUE -> setQueued()
Page.LOAD_PAGE -> onLoading() Page.LOAD_PAGE -> setLoading()
Page.DOWNLOAD_IMAGE -> onLoading() Page.DOWNLOAD_IMAGE -> {
Page.READY -> onReady() observeProgress()
Page.ERROR -> onError() setDownloading()
}
Page.READY -> {
setImage()
unsubscribeProgress()
}
Page.ERROR -> {
setError()
unsubscribeProgress()
}
} }
} }
/** /**
* Unsubscribes from the status subscription. * Unsubscribes from the status subscription.
*/ */
fun unsubscribeStatus() { private fun unsubscribeStatus() {
page?.setStatusSubject(null)
statusSubscription?.unsubscribe() statusSubscription?.unsubscribe()
statusSubscription = null statusSubscription = null
} }
/**
* Unsubscribes from the progress subscription.
*/
private fun unsubscribeProgress() {
progressSubscription?.unsubscribe()
progressSubscription = null
}
/**
* Called when the page is queued.
*/
private fun setQueued() = with(view) {
progress_container.visibility = View.VISIBLE
progress_text.visibility = View.INVISIBLE
retry_button.visibility = View.GONE
decodeErrorLayout?.let {
(view as ViewGroup).removeView(it)
decodeErrorLayout = null
}
}
/** /**
* Called when the page is loading. * Called when the page is loading.
*/ */
private fun onLoading() { private fun setLoading() = with(view) {
setRetryButtonVisible(false) progress_container.visibility = View.VISIBLE
setImageVisible(false) progress_text.visibility = View.VISIBLE
setProgressVisible(true) progress_text.setText(R.string.downloading)
}
/**
* Called when the page is downloading
*/
private fun setDownloading() = with(view) {
progress_container.visibility = View.VISIBLE
progress_text.visibility = View.VISIBLE
} }
/** /**
* Called when the page is ready. * Called when the page is ready.
*/ */
private fun onReady() { private fun setImage() = with(view) {
setRetryButtonVisible(false) val path = page?.imagePath
setProgressVisible(false) if (path != null && File(path).exists()) {
setImageVisible(true) progress_text.visibility = View.INVISIBLE
image_view.setImage(ImageSource.uri(path))
page?.imagePath?.let { path ->
if (File(path).exists()) {
view.image_view.setImage(ImageSource.uri(path))
view.progress.visibility = View.GONE
} else { } else {
page?.status = Page.ERROR page?.status = Page.ERROR
} }
} }
}
/** /**
* Called when the page has an error. * Called when the page has an error.
*/ */
private fun onError() { private fun setError() = with(view) {
setImageVisible(false) progress_container.visibility = View.GONE
setProgressVisible(false) retry_button.visibility = View.VISIBLE
setRetryButtonVisible(true)
} }
/** /**
* Called when the page is queued. * Called when the image is decoded and going to be displayed.
*/ */
private fun onQueue() { private fun onImageDecoded() {
setImageVisible(false) view.progress_container.visibility = View.GONE
setRetryButtonVisible(false)
setProgressVisible(false)
} }
/** /**
* Called when the image fails to decode. * Called when the image fails to decode.
*/ */
private fun onImageDecodeError() { private fun onImageDecodeError() {
page?.let { page -> val page = page ?: return
if (decodeErrorLayout != null || !webtoonReader.isAdded) return
decodeErrorLayout = PageDecodeErrorLayout(view.context, page, readerActivity.readerTheme, decodeErrorLayout = PageDecodeErrorLayout(view.context, page, readerActivity.readerTheme,
{ readerActivity.presenter.retryPage(page) }) { readerActivity.presenter.retryPage(page) })
(view as ViewGroup).addView(decodeErrorLayout) (view as ViewGroup).addView(decodeErrorLayout)
} }
}
/**
* Sets the visibility of the progress bar.
*
* @param visible whether to show it or not.
*/
private fun setProgressVisible(visible: Boolean) {
view.progress.visibility = if (visible) View.VISIBLE else View.GONE
}
/**
* Sets the visibility of the image view.
*
* @param visible whether to show it or not.
*/
private fun setImageVisible(visible: Boolean) {
view.image_view.visibility = if (visible) View.VISIBLE else View.GONE
}
/**
* Sets the visibility of the retry button.
*
* @param visible whether to show it or not.
*/
private fun setRetryButtonVisible(visible: Boolean) {
view.retry_button.visibility = if (visible) View.VISIBLE else View.GONE
}
/** /**
* Property to get the reader activity. * Property to get the reader activity.

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<eu.kanade.tachiyomi.ui.reader.viewer.pager.PageView <eu.kanade.tachiyomi.ui.reader.viewer.pager.PageView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View file

@ -1,31 +1,43 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android">
<FrameLayout <LinearLayout
android:id="@+id/frame_container" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="match_parent"> android:layout_gravity="center_horizontal"
android:layout_marginTop="32dp"
android:id="@+id/progress_container"
android:orientation="vertical">
<ProgressBar <ProgressBar
android:id="@+id/progress" android:id="@+id/progress"
style="?android:attr/progressBarStyleLarge" style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal"/> android:layout_gravity="center_horizontal"/>
<Button <TextView
android:id="@+id/retry_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal" android:layout_marginTop="16dp"
android:text="@string/action_retry" android:id="@+id/progress_text"
android:visibility="gone"/> android:layout_gravity="center"
android:visibility="invisible"
android:textSize="16sp" />
</FrameLayout> </LinearLayout>
<include layout="@layout/chapter_image"/> <include layout="@layout/chapter_image"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/retry_button"
android:text="@string/action_retry"
android:layout_gravity="center"
android:visibility="gone"/>
</FrameLayout> </FrameLayout>