ソースを参照

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

len 8 年 前
コミット
90e0e0b72a

+ 1 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/PageDecodeErrorLayout.kt

@@ -29,7 +29,7 @@ class PageDecodeErrorLayout(context: Context) : LinearLayout(context) {
 
     init {
         orientation = LinearLayout.VERTICAL
-        setGravity(Gravity.CENTER)
+        gravity = Gravity.CENTER
     }
 
     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)
             setText(R.string.action_retry)
             setOnClickListener {
-                removeAllViews()
                 retryListener()
             }
             addView(this)

+ 62 - 55
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PageView.kt

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

+ 3 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt

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

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt

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

+ 111 - 79
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt

@@ -6,16 +6,19 @@ import android.view.View
 import android.view.ViewGroup
 import com.davemorrissey.labs.subscaleview.ImageSource
 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
+import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.source.model.Page
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
 import kotlinx.android.synthetic.main.chapter_image.view.*
 import kotlinx.android.synthetic.main.item_webtoon_reader.view.*
+import rx.Observable
 import rx.Subscription
 import rx.android.schedulers.AndroidSchedulers
 import rx.subjects.PublishSubject
 import rx.subjects.SerializedSubject
 import java.io.File
+import java.util.concurrent.TimeUnit
 
 /**
  * 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
 
+    /**
+     * Subscription for progress changes of the page.
+     */
+    private var progressSubscription: Subscription? = null
+
     /**
      * Layout of decode error.
      */
@@ -57,8 +65,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
             setOnTouchListener(adapter.touchListener)
             setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
                 override fun onImageLoaded() {
-                    // When the image is loaded, reset the minimum height to avoid gaps
-                    view.frame_container.minimumHeight = 30
+                    onImageDecoded()
                 }
 
                 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,
-        // 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
+        view.progress_container.minimumHeight = view.resources.displayMetrics.heightPixels
 
-        // Leave some space between progress bars
-        view.progress.minimumHeight = 300
-
-        view.frame_container.setOnTouchListener(adapter.touchListener)
+        view.setOnTouchListener(adapter.touchListener)
         view.retry_button.setOnTouchListener { v, event ->
             if (event.action == MotionEvent.ACTION_UP) {
                 readerActivity.presenter.retryPage(page)
@@ -92,13 +92,22 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
      * @param page the page to bind.
      */
     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 {
             (view as ViewGroup).removeView(it)
             decodeErrorLayout = null
         }
-
-        this.page = page
-        observeStatus()
+        view.image_view.recycle()
+        view.progress_container.visibility = View.VISIBLE
     }
 
     /**
@@ -107,17 +116,35 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
      * @see processStatus
      */
     private fun observeStatus() {
-        page?.let { page ->
-            val statusSubject = SerializedSubject(PublishSubject.create<Int>())
-            page.setStatusSubject(statusSubject)
+        val page = page ?: return
 
-            statusSubscription?.unsubscribe()
-            statusSubscription = statusSubject.startWith(page.status)
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .subscribe { processStatus(it) }
+        val statusSubject = SerializedSubject(PublishSubject.create<Int>())
+        page.setStatusSubject(statusSubject)
 
-            webtoonReader.subscriptions.add(statusSubscription)
-        }
+        statusSubscription?.unsubscribe()
+        statusSubscription = statusSubject.startWith(page.status)
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe { processStatus(it) }
+
+        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,104 +154,109 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
      */
     private fun processStatus(status: Int) {
         when (status) {
-            Page.QUEUE -> onQueue()
-            Page.LOAD_PAGE -> onLoading()
-            Page.DOWNLOAD_IMAGE -> onLoading()
-            Page.READY -> onReady()
-            Page.ERROR -> onError()
+            Page.QUEUE -> setQueued()
+            Page.LOAD_PAGE -> setLoading()
+            Page.DOWNLOAD_IMAGE -> {
+                observeProgress()
+                setDownloading()
+            }
+            Page.READY -> {
+                setImage()
+                unsubscribeProgress()
+            }
+            Page.ERROR -> {
+                setError()
+                unsubscribeProgress()
+            }
         }
     }
 
     /**
      * Unsubscribes from the status subscription.
      */
-    fun unsubscribeStatus() {
+    private fun unsubscribeStatus() {
+        page?.setStatusSubject(null)
         statusSubscription?.unsubscribe()
         statusSubscription = null
     }
 
     /**
-     * Called when the page is loading.
+     * Unsubscribes from the progress subscription.
      */
-    private fun onLoading() {
-        setRetryButtonVisible(false)
-        setImageVisible(false)
-        setProgressVisible(true)
+    private fun unsubscribeProgress() {
+        progressSubscription?.unsubscribe()
+        progressSubscription = null
     }
 
     /**
-     * Called when the page is ready.
+     * Called when the page is queued.
      */
-    private fun onReady() {
-        setRetryButtonVisible(false)
-        setProgressVisible(false)
-        setImageVisible(true)
-
-        page?.imagePath?.let { path ->
-            if (File(path).exists()) {
-                view.image_view.setImage(ImageSource.uri(path))
-                view.progress.visibility = View.GONE
-            } else {
-                page?.status = Page.ERROR
-            }
+    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 has an error.
+     * Called when the page is loading.
      */
-    private fun onError() {
-        setImageVisible(false)
-        setProgressVisible(false)
-        setRetryButtonVisible(true)
+    private fun setLoading() = with(view) {
+        progress_container.visibility = View.VISIBLE
+        progress_text.visibility = View.VISIBLE
+        progress_text.setText(R.string.downloading)
     }
 
     /**
-     * Called when the page is queued.
+     * Called when the page is downloading
      */
-    private fun onQueue() {
-        setImageVisible(false)
-        setRetryButtonVisible(false)
-        setProgressVisible(false)
+    private fun setDownloading() = with(view) {
+        progress_container.visibility = View.VISIBLE
+        progress_text.visibility = View.VISIBLE
     }
 
     /**
-     * Called when the image fails to decode.
+     * Called when the page is ready.
      */
-    private fun onImageDecodeError() {
-        page?.let { page ->
-            decodeErrorLayout = PageDecodeErrorLayout(view.context, page, readerActivity.readerTheme,
-                    { readerActivity.presenter.retryPage(page) })
-
-            (view as ViewGroup).addView(decodeErrorLayout)
+    private fun setImage() = with(view) {
+        val path = page?.imagePath
+        if (path != null && File(path).exists()) {
+            progress_text.visibility = View.INVISIBLE
+            image_view.setImage(ImageSource.uri(path))
+        } else {
+            page?.status = Page.ERROR
         }
     }
 
     /**
-     * Sets the visibility of the progress bar.
-     *
-     * @param visible whether to show it or not.
+     * Called when the page has an error.
      */
-    private fun setProgressVisible(visible: Boolean) {
-        view.progress.visibility = if (visible) View.VISIBLE else View.GONE
+    private fun setError() = with(view) {
+        progress_container.visibility = View.GONE
+        retry_button.visibility = View.VISIBLE
     }
 
     /**
-     * Sets the visibility of the image view.
-     *
-     * @param visible whether to show it or not.
+     * Called when the image is decoded and going to be displayed.
      */
-    private fun setImageVisible(visible: Boolean) {
-        view.image_view.visibility = if (visible) View.VISIBLE else View.GONE
+    private fun onImageDecoded() {
+        view.progress_container.visibility = View.GONE
     }
 
     /**
-     * Sets the visibility of the retry button.
-     *
-     * @param visible whether to show it or not.
+     * Called when the image fails to decode.
      */
-    private fun setRetryButtonVisible(visible: Boolean) {
-        view.retry_button.visibility = if (visible) View.VISIBLE else View.GONE
+    private fun onImageDecodeError() {
+        val page = page ?: return
+        if (decodeErrorLayout != null || !webtoonReader.isAdded) return
+
+        decodeErrorLayout = PageDecodeErrorLayout(view.context, page, readerActivity.readerTheme,
+                { readerActivity.presenter.retryPage(page) })
+
+        (view as ViewGroup).addView(decodeErrorLayout)
     }
 
     /**

+ 0 - 1
app/src/main/res/layout/item_pager_reader.xml

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

+ 25 - 13
app/src/main/res/layout/item_webtoon_reader.xml

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