Quellcode durchsuchen

Readers in Kotlin. Also fix #193

len vor 9 Jahren
Ursprung
Commit
ff61282104
32 geänderte Dateien mit 1730 neuen und 1394 gelöschten Zeilen
  1. 7 6
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.java
  2. 10 5
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderMenu.java
  3. 0 130
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.java
  4. 222 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt
  5. 0 67
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/PageDecodeErrorLayout.java
  6. 65 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/PageDecodeErrorLayout.kt
  7. 0 6
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/OnChapterBoundariesOutListener.java
  8. 6 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/OnChapterBoundariesOutListener.kt
  9. 0 196
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.java
  10. 287 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt
  11. 0 60
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.java
  12. 78 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.kt
  13. 0 270
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderFragment.java
  14. 294 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderFragment.kt
  15. 0 87
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/HorizontalPager.java
  16. 86 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/HorizontalPager.kt
  17. 0 19
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/LeftToRightReader.java
  18. 19 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/LeftToRightReader.kt
  19. 0 30
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/RightToLeftReader.java
  20. 30 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/RightToLeftReader.kt
  21. 0 86
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalPager.java
  22. 84 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalPager.kt
  23. 0 19
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalReader.java
  24. 19 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalReader.kt
  25. 0 72
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.java
  26. 78 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt
  27. 0 135
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.java
  28. 240 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.kt
  29. 0 204
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.java
  30. 203 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt
  31. 1 1
      app/src/main/res/layout/chapter_image.xml
  32. 1 1
      app/src/main/res/layout/item_webtoon_reader.xml

+ 7 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.java

@@ -97,13 +97,14 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
     @Override
     protected void onPause() {
         if (viewer != null)
-            getPresenter().setCurrentPage(viewer.getCurrentPage());
+            getPresenter().setCurrentPage(viewer.getActivePage());
         super.onPause();
     }
 
     @Override
     protected void onDestroy() {
         subscriptions.unsubscribe();
+        readerMenu.destroy();
         viewer = null;
         super.onDestroy();
     }
@@ -127,7 +128,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
     @Override
     public void onBackPressed() {
         if (viewer != null)
-            getPresenter().setCurrentPage(viewer.getCurrentPage());
+            getPresenter().setCurrentPage(viewer.getActivePage());
         getPresenter().onChapterLeft();
 
         int chapterToUpdate = getPresenter().getMangaSyncChapterToUpdate();
@@ -255,8 +256,8 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
     }
 
     public void gotoPageInCurrentChapter(int pageIndex) {
-        Page requestedPage = viewer.getCurrentPage().getChapter().getPages().get(pageIndex);
-        viewer.setSelectedPage(requestedPage);
+        Page requestedPage = viewer.getActivePage().getChapter().getPages().get(pageIndex);
+        viewer.setActivePage(requestedPage);
     }
 
     public void onCenterSingleTap() {
@@ -264,7 +265,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
     }
 
     public void requestNextChapter() {
-        getPresenter().setCurrentPage(viewer.getCurrentPage());
+        getPresenter().setCurrentPage(viewer.getActivePage());
         if (!getPresenter().loadNextChapter()) {
             ToastUtil.showShort(this, R.string.no_next_chapter);
         }
@@ -272,7 +273,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
     }
 
     public void requestPreviousChapter() {
-        getPresenter().setCurrentPage(viewer.getCurrentPage());
+        getPresenter().setCurrentPage(viewer.getActivePage());
         if (!getPresenter().loadPreviousChapter()) {
             ToastUtil.showShort(this, R.string.no_previous_chapter);
         }

+ 10 - 5
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderMenu.java

@@ -31,7 +31,6 @@ import eu.kanade.tachiyomi.R;
 import eu.kanade.tachiyomi.data.database.models.Chapter;
 import eu.kanade.tachiyomi.data.database.models.Manga;
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
-import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
 import icepick.State;
 import rx.Subscription;
 
@@ -116,6 +115,12 @@ public class ReaderMenu {
         showing = false;
     }
 
+    public void destroy() {
+        if (settingsPopup != null) {
+            settingsPopup.dismiss();
+        }
+    }
+
     public boolean onCreateOptionsMenu(Menu menu) {
         activity.getMenuInflater().inflate(R.menu.reader, menu);
         nextChapterBtn = menu.findItem(R.id.action_next_chapter);
@@ -349,12 +354,12 @@ public class ReaderMenu {
         private void setDecoderInitial(int decoder) {
             String initial;
             switch (decoder) {
-                case BaseReader.SKIA_DECODER:
-                    initial = "S";
-                    break;
-                case BaseReader.RAPID_DECODER:
+                case 0:
                     initial = "R";
                     break;
+                case 1:
+                    initial = "S";
+                    break;
                 default:
                     initial = "";
                     break;

+ 0 - 130
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.java

@@ -1,130 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.base;
-
-import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder;
-import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder;
-import com.davemorrissey.labs.subscaleview.decoder.RapidImageRegionDecoder;
-import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder;
-import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import eu.kanade.tachiyomi.data.database.models.Chapter;
-import eu.kanade.tachiyomi.data.source.model.Page;
-import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
-
-public abstract class BaseReader extends BaseFragment {
-
-    protected int currentPage;
-    protected List<Page> pages;
-    protected List<Chapter> chapters;
-    protected Class<? extends ImageRegionDecoder> regionDecoderClass;
-    protected Class<? extends ImageDecoder> bitmapDecoderClass;
-
-    private boolean hasRequestedNextChapter;
-
-    public static final int RAPID_DECODER = 0;
-    public static final int SKIA_DECODER = 1;
-
-    public void updatePageNumber() {
-        getReaderActivity().onPageChanged(getCurrentPage().getPageNumber(), getCurrentPage().getChapter().getPages().size());
-    }
-
-    public Page getCurrentPage() {
-        return pages.get(currentPage);
-    }
-
-    public void onPageChanged(int position) {
-        Page oldPage = pages.get(currentPage);
-        Page newPage = pages.get(position);
-        newPage.getChapter().last_page_read = newPage.getPageNumber();
-
-        if (getReaderActivity().getPresenter().isSeamlessMode()) {
-            Chapter oldChapter = oldPage.getChapter();
-            Chapter newChapter = newPage.getChapter();
-            if (!hasRequestedNextChapter && position > pages.size() - 5) {
-                hasRequestedNextChapter = true;
-                getReaderActivity().getPresenter().appendNextChapter();
-            }
-            if (!oldChapter.id.equals(newChapter.id)) {
-                onChapterChanged(newPage.getChapter(), newPage);
-            }
-        }
-        currentPage = position;
-        updatePageNumber();
-    }
-
-    private void onChapterChanged(Chapter chapter, Page currentPage) {
-        getReaderActivity().onEnterChapter(chapter, currentPage.getPageNumber());
-    }
-
-    public void setSelectedPage(Page page) {
-        setSelectedPage(getPageIndex(page));
-    }
-
-    public int getPageIndex(Page search) {
-        // search for the index of a page in the current list without requiring them to be the same object
-        for (Page page : pages) {
-            if (page.getPageNumber() == search.getPageNumber() &&
-                    page.getChapter().id.equals(search.getChapter().id)) {
-                return pages.indexOf(page);
-            }
-        }
-        return 0;
-    }
-
-    public void onPageListReady(Chapter chapter, Page currentPage) {
-        if (chapters == null || !chapters.contains(chapter)) {
-            // if we reset the loaded page we also need to reset the loaded chapters
-            chapters = new ArrayList<>();
-            chapters.add(chapter);
-            onSetChapter(chapter, currentPage);
-        } else {
-            setSelectedPage(currentPage);
-        }
-    }
-
-    public void onPageListAppendReady(Chapter chapter) {
-        if (!chapters.contains(chapter)) {
-            hasRequestedNextChapter = false;
-            chapters.add(chapter);
-            onAppendChapter(chapter);
-        }
-    }
-
-    public abstract void setSelectedPage(int pageNumber);
-    public abstract void onSetChapter(Chapter chapter, Page currentPage);
-    public abstract void onAppendChapter(Chapter chapter);
-    public abstract void moveToNext();
-    public abstract void moveToPrevious();
-
-    public void setDecoderClass(int value) {
-        switch (value) {
-            case RAPID_DECODER:
-            default:
-                regionDecoderClass = RapidImageRegionDecoder.class;
-                bitmapDecoderClass = SkiaImageDecoder.class;
-                // Using Skia because Rapid isn't stable. Rapid is still used for region decoding.
-                // https://github.com/inorichi/tachiyomi/issues/97
-                //bitmapDecoderClass = RapidImageDecoder.class;
-                break;
-            case SKIA_DECODER:
-                regionDecoderClass = SkiaImageRegionDecoder.class;
-                bitmapDecoderClass = SkiaImageDecoder.class;
-                break;
-        }
-    }
-
-    public Class<? extends ImageRegionDecoder> getRegionDecoderClass() {
-        return regionDecoderClass;
-    }
-
-    public Class<? extends ImageDecoder> getBitmapDecoderClass() {
-        return bitmapDecoderClass;
-    }
-
-    public ReaderActivity getReaderActivity() {
-        return (ReaderActivity) getActivity();
-    }
-}

+ 222 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/BaseReader.kt

@@ -0,0 +1,222 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.base
+
+import com.davemorrissey.labs.subscaleview.decoder.*
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import java.util.*
+
+/**
+ * Base reader containing the common data that can be used by its implementations. It does not
+ * contain any UI related action.
+ */
+abstract class BaseReader : BaseFragment() {
+
+    companion object {
+        /**
+         * Rapid decoder.
+         */
+        const val RAPID_DECODER = 0
+
+        /**
+         * Skia decoder.
+         */
+        const val SKIA_DECODER = 1
+    }
+
+    /**
+     * List of chapters added in the reader.
+     */
+    private var chapters = ArrayList<Chapter>()
+
+    /**
+     * List of pages added in the reader. It can contain pages from more than one chapter.
+     */
+    var pages: MutableList<Page> = ArrayList()
+        private set
+
+    /**
+     * Current visible position of [pages].
+     */
+    var currentPage: Int = 0
+        protected set
+
+    /**
+     * Region decoder class to use.
+     */
+    lateinit var regionDecoderClass: Class<out ImageRegionDecoder>
+        private set
+
+    /**
+     * Bitmap decoder class to use.
+     */
+    lateinit var bitmapDecoderClass: Class<out ImageDecoder>
+        private set
+
+    /**
+     * Whether the reader has requested to append a chapter. Used with seamless mode to avoid
+     * restarting requests when changing pages.
+     */
+    private var hasRequestedNextChapter: Boolean = false
+
+    /**
+     * Updates the reader activity with the active page.
+     */
+    fun updatePageNumber() {
+        val activePage = getActivePage()
+        readerActivity.onPageChanged(activePage.pageNumber, activePage.chapter.pages.size)
+    }
+
+    /**
+     * Returns the active page.
+     */
+    fun getActivePage(): Page {
+        return pages[currentPage]
+    }
+
+    /**
+     * Called when a page changes. Implementations must call this method.
+     *
+     * @param position the new current page.
+     */
+    fun onPageChanged(position: Int) {
+        val oldPage = pages[currentPage]
+        val newPage = pages[position]
+        newPage.chapter.last_page_read = newPage.pageNumber
+
+        if (readerActivity.presenter.isSeamlessMode) {
+            val oldChapter = oldPage.chapter
+            val newChapter = newPage.chapter
+            if (!hasRequestedNextChapter && position > pages.size - 5) {
+                hasRequestedNextChapter = true
+                readerActivity.presenter.appendNextChapter()
+            }
+            if (oldChapter.id != newChapter.id) {
+                // Active chapter has changed.
+                readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber)
+            }
+        }
+        currentPage = position
+        updatePageNumber()
+    }
+
+    /**
+     * Sets the active page.
+     *
+     * @param page the page to display.
+     */
+    fun setActivePage(page: Page) {
+        setActivePage(getPageIndex(page))
+    }
+
+    /**
+     * Searchs for the index of a page in the current list without requiring them to be the same
+     * object.
+     *
+     * @param search the page to search.
+     * @return the index of the page in [pages] or 0 if it's not found.
+     */
+    fun getPageIndex(search: Page): Int {
+        for ((index, page) in pages.withIndex()) {
+            if (page.pageNumber == search.pageNumber && page.chapter.id == search.chapter.id) {
+                return index
+            }
+        }
+        return 0
+    }
+
+    /**
+     * Called from the presenter when the page list of a chapter is ready. This method is called
+     * on every [onResume], so we add some logic to avoid duplicating chapters.
+     *
+     * @param chapter the chapter to set.
+     * @param currentPage the initial page to display.
+     */
+    fun onPageListReady(chapter: Chapter, currentPage: Page) {
+        if (!chapters.contains(chapter)) {
+            // if we reset the loaded page we also need to reset the loaded chapters
+            chapters = ArrayList<Chapter>()
+            chapters.add(chapter)
+            pages = ArrayList(chapter.pages)
+            onChapterSet(chapter, currentPage)
+        } else {
+            setActivePage(currentPage)
+        }
+    }
+
+    /**
+     * Called from the presenter when the page list of a chapter to append is ready. This method is
+     * called on every [onResume], so we add some logic to avoid duplicating chapters.
+     *
+     * @param chapter the chapter to append.
+     */
+    fun onPageListAppendReady(chapter: Chapter) {
+        if (!chapters.contains(chapter)) {
+            hasRequestedNextChapter = false
+            chapters.add(chapter)
+            pages.addAll(chapter.pages)
+            onChapterAppended(chapter)
+        }
+    }
+
+    /**
+     * Sets the active page.
+     *
+     * @param pageNumber the index of the page from [pages].
+     */
+    abstract fun setActivePage(pageNumber: Int)
+
+    /**
+     * Called when a new chapter is set in [BaseReader].
+     *
+     * @param chapter the chapter set.
+     * @param currentPage the initial page to display.
+     */
+    abstract fun onChapterSet(chapter: Chapter, currentPage: Page)
+
+    /**
+     * Called when a chapter is appended in [BaseReader].
+     *
+     * @param chapter the chapter appended.
+     */
+    abstract fun onChapterAppended(chapter: Chapter)
+
+    /**
+     * Moves pages forward. Implementations decide how to move (by a page, by some distance...).
+     */
+    abstract fun moveToNext()
+
+    /**
+     * Moves pages backward. Implementations decide how to move (by a page, by some distance...).
+     */
+    abstract fun moveToPrevious()
+
+    /**
+     * Sets the active decoder class.
+     *
+     * @param value the decoder class to use.
+     */
+    fun setDecoderClass(value: Int) {
+        when (value) {
+            RAPID_DECODER -> {
+                // Using Skia because Rapid isn't stable. Rapid is still used for region decoding.
+                // https://github.com/inorichi/tachiyomi/issues/97
+                //bitmapDecoderClass = RapidImageDecoder.class;
+                regionDecoderClass = RapidImageRegionDecoder::class.java
+                bitmapDecoderClass = SkiaImageDecoder::class.java
+            }
+            SKIA_DECODER -> {
+                regionDecoderClass = SkiaImageRegionDecoder::class.java
+                bitmapDecoderClass = SkiaImageDecoder::class.java
+            }
+        }
+    }
+
+    /**
+     * Property to get the reader activity.
+     */
+    val readerActivity: ReaderActivity
+        get() = activity as ReaderActivity
+
+}

+ 0 - 67
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/base/PageDecodeErrorLayout.java

@@ -1,67 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.base;
-
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.support.v4.content.ContextCompat;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import eu.kanade.tachiyomi.R;
-import eu.kanade.tachiyomi.data.source.model.Page;
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
-import rx.functions.Action0;
-
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-
-public class PageDecodeErrorLayout extends LinearLayout {
-
-    private final int lightGreyColor;
-    private final int blackColor;
-
-    public PageDecodeErrorLayout(Context context) {
-        super(context);
-        setOrientation(LinearLayout.VERTICAL);
-        setGravity(Gravity.CENTER);
-
-        lightGreyColor = ContextCompat.getColor(context, R.color.light_grey);
-        blackColor = ContextCompat.getColor(context, R.color.primary_text);
-    }
-
-    public PageDecodeErrorLayout(Context context, Page page, int theme, Action0 retryListener) {
-        this(context);
-
-        TextView errorText = new TextView(context);
-        errorText.setGravity(Gravity.CENTER);
-        errorText.setText(R.string.decode_image_error);
-        errorText.setTextColor(theme == ReaderActivity.BLACK_THEME ? lightGreyColor : blackColor);
-
-        Button retryButton = new Button(context);
-        retryButton.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
-        retryButton.setText(R.string.action_retry);
-        retryButton.setOnClickListener((v) -> {
-            removeAllViews();
-            retryListener.call();
-        });
-
-        Button openInBrowserButton = new Button(context);
-        openInBrowserButton.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
-        openInBrowserButton.setText(R.string.action_open_in_browser);
-        openInBrowserButton.setOnClickListener((v) -> {
-            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(page.getImageUrl()));
-            context.startActivity(intent);
-        });
-
-        if (page.getImageUrl() == null) {
-            openInBrowserButton.setVisibility(View.GONE);
-        }
-
-        addView(errorText);
-        addView(retryButton);
-        addView(openInBrowserButton);
-    }
-}

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

@@ -0,0 +1,65 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.base
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.support.v4.content.ContextCompat
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.TextView
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+
+class PageDecodeErrorLayout(context: Context) : LinearLayout(context) {
+
+    private val lightGreyColor = ContextCompat.getColor(context, R.color.light_grey)
+    private val blackColor = ContextCompat.getColor(context, R.color.primary_text)
+
+    init {
+        orientation = LinearLayout.VERTICAL
+        setGravity(Gravity.CENTER)
+    }
+
+    constructor(context: Context, page: Page, theme: Int, retryListener: () -> Unit) : this(context) {
+
+        // Error message.
+        TextView(context).apply {
+            gravity = Gravity.CENTER
+            setText(R.string.decode_image_error)
+            setTextColor(if (theme == ReaderActivity.BLACK_THEME) lightGreyColor else blackColor)
+            addView(this)
+        }
+
+        // Retry button.
+        Button(context).apply {
+            layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
+            setText(R.string.action_retry)
+            setOnClickListener {
+                removeAllViews()
+                retryListener()
+            }
+            addView(this)
+        }
+
+        // Open in browser button.
+        Button(context).apply {
+            layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
+            setText(R.string.action_open_in_browser)
+            setOnClickListener { v ->
+                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(page.imageUrl))
+                context.startActivity(intent)
+            }
+
+            if (page.imageUrl == null) {
+                visibility = View.GONE
+            }
+            addView(this)
+        }
+
+    }
+}

+ 0 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/OnChapterBoundariesOutListener.java

@@ -1,6 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager;
-
-public interface OnChapterBoundariesOutListener {
-    void onFirstPageOutEvent();
-    void onLastPageOutEvent();
-}

+ 6 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/OnChapterBoundariesOutListener.kt

@@ -0,0 +1,6 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.pager
+
+interface OnChapterBoundariesOutListener {
+    fun onFirstPageOutEvent()
+    fun onLastPageOutEvent()
+}

+ 0 - 196
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.java

@@ -1,196 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager;
-
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.ViewGroup;
-
-import java.util.ArrayList;
-
-import eu.kanade.tachiyomi.R;
-import eu.kanade.tachiyomi.data.database.models.Chapter;
-import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
-import eu.kanade.tachiyomi.data.source.model.Page;
-import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader;
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader;
-import rx.subscriptions.CompositeSubscription;
-
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-
-public abstract class PagerReader extends BaseReader {
-
-    protected PagerReaderAdapter adapter;
-    protected Pager pager;
-    protected GestureDetector gestureDetector;
-
-    protected boolean transitions;
-    protected CompositeSubscription subscriptions;
-
-    protected int scaleType = 1;
-    protected int zoomStart = 1;
-
-    public static final int ALIGN_AUTO = 1;
-    public static final int ALIGN_LEFT = 2;
-    public static final int ALIGN_RIGHT = 3;
-    public static final int ALIGN_CENTER = 4;
-
-    private static final float LEFT_REGION = 0.33f;
-    private static final float RIGHT_REGION = 0.66f;
-
-    protected void initializePager(Pager pager) {
-        this.pager = pager;
-        pager.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
-        pager.setOffscreenPageLimit(1);
-        pager.setId(R.id.view_pager);
-        pager.setOnChapterBoundariesOutListener(new OnChapterBoundariesOutListener() {
-            @Override
-            public void onFirstPageOutEvent() {
-                getReaderActivity().requestPreviousChapter();
-            }
-
-            @Override
-            public void onLastPageOutEvent() {
-                getReaderActivity().requestNextChapter();
-            }
-        });
-        gestureDetector = createGestureDetector();
-
-        adapter = new PagerReaderAdapter(getChildFragmentManager());
-        pager.setAdapter(adapter);
-
-        PreferencesHelper preferences = getReaderActivity().getPreferences();
-        subscriptions = new CompositeSubscription();
-        subscriptions.add(preferences.imageDecoder()
-                .asObservable()
-                .doOnNext(this::setDecoderClass)
-                .skip(1)
-                .distinctUntilChanged()
-                .subscribe(v -> refreshPages()));
-
-        subscriptions.add(preferences.imageScaleType()
-                .asObservable()
-                .doOnNext(this::setImageScaleType)
-                .skip(1)
-                .distinctUntilChanged()
-                .subscribe(v -> refreshPages()));
-
-        subscriptions.add(preferences.zoomStart()
-                .asObservable()
-                .doOnNext(this::setZoomStart)
-                .skip(1)
-                .distinctUntilChanged()
-                .subscribe(v -> refreshPages()));
-
-        subscriptions.add(preferences.enableTransitions()
-                .asObservable()
-                .subscribe(value -> transitions = value));
-
-        setPages();
-    }
-
-    @Override
-    public void onDestroyView() {
-        subscriptions.unsubscribe();
-        super.onDestroyView();
-    }
-
-    protected GestureDetector createGestureDetector() {
-        return new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
-            @Override
-            public boolean onSingleTapConfirmed(MotionEvent e) {
-                final float positionX = e.getX();
-
-                if (positionX < pager.getWidth() * LEFT_REGION) {
-                    onLeftSideTap();
-                } else if (positionX > pager.getWidth() * RIGHT_REGION) {
-                    onRightSideTap();
-                } else {
-                    getReaderActivity().onCenterSingleTap();
-                }
-                return true;
-            }
-        });
-    }
-
-    @Override
-    public void onSetChapter(Chapter chapter, Page currentPage) {
-        pages = new ArrayList<>(chapter.getPages());
-        this.currentPage = getPageIndex(currentPage); // we might have a new page object
-
-        // This method can be called before the view is created
-        if (pager != null) {
-            setPages();
-        }
-    }
-
-    public void onAppendChapter(Chapter chapter) {
-        pages.addAll(chapter.getPages());
-
-        // This method can be called before the view is created
-        if (pager != null) {
-            adapter.setPages(pages);
-        }
-    }
-
-    protected void setPages() {
-        if (pages != null) {
-            pager.clearOnPageChangeListeners();
-            adapter.setPages(pages);
-            setSelectedPage(currentPage);
-            updatePageNumber();
-            pager.setOnPageChangeListener(this::onPageChanged);
-        }
-    }
-
-    @Override
-    public void setSelectedPage(int pageNumber) {
-        pager.setCurrentItem(pageNumber, false);
-    }
-
-    private void refreshPages() {
-        pager.setAdapter(adapter);
-        pager.setCurrentItem(currentPage, false);
-    }
-
-    protected void onLeftSideTap() {
-        moveToPrevious();
-    }
-
-    protected void onRightSideTap() {
-        moveToNext();
-    }
-
-    public void moveToNext() {
-        if (pager.getCurrentItem() != pager.getAdapter().getCount() - 1) {
-            pager.setCurrentItem(pager.getCurrentItem() + 1, transitions);
-        } else {
-            getReaderActivity().requestNextChapter();
-        }
-    }
-
-    public void moveToPrevious() {
-        if (pager.getCurrentItem() != 0) {
-            pager.setCurrentItem(pager.getCurrentItem() - 1, transitions);
-        } else {
-            getReaderActivity().requestPreviousChapter();
-        }
-    }
-
-    private void setImageScaleType(int scaleType) {
-        this.scaleType = scaleType;
-    }
-
-    private void setZoomStart(int zoomStart) {
-        if (zoomStart == ALIGN_AUTO) {
-            if (this instanceof LeftToRightReader)
-                setZoomStart(ALIGN_LEFT);
-            else if (this instanceof RightToLeftReader)
-                setZoomStart(ALIGN_RIGHT);
-            else
-                setZoomStart(ALIGN_CENTER);
-        } else {
-            this.zoomStart = zoomStart;
-        }
-    }
-
-}

+ 287 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReader.kt

@@ -0,0 +1,287 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.pager
+
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
+import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
+import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader
+import rx.subscriptions.CompositeSubscription
+
+/**
+ * Implementation of a reader based on a ViewPager.
+ */
+abstract class PagerReader : BaseReader() {
+
+    companion object {
+        /**
+         * Zoom automatic alignment.
+         */
+        const val ALIGN_AUTO = 1
+
+        /**
+         * Align to left.
+         */
+        const val ALIGN_LEFT = 2
+
+        /**
+         * Align to right.
+         */
+        const val ALIGN_RIGHT = 3
+
+        /**
+         * Align to right.
+         */
+        const val ALIGN_CENTER = 4
+
+        /**
+         * Left side region of the screen. Used for touch events.
+         */
+        const val LEFT_REGION = 0.33f
+
+        /**
+         * Right side region of the screen. Used for touch events.
+         */
+        const val RIGHT_REGION = 0.66f
+    }
+
+    /**
+     * Generic interface of a ViewPager.
+     */
+    lateinit var pager: Pager
+        private set
+
+    /**
+     * Adapter of the pager.
+     */
+    lateinit var adapter: PagerReaderAdapter
+        private set
+
+    /**
+     * Gesture detector for touch events.
+     */
+    val gestureDetector by lazy { createGestureDetector() }
+
+    /**
+     * Subscriptions for reader settings.
+     */
+    var subscriptions: CompositeSubscription? = null
+        private set
+
+    /**
+     * Whether transitions are enabled or not.
+     */
+    var transitions: Boolean = false
+        private set
+
+    /**
+     * Scale type (fit width, fit screen, etc).
+     */
+    var scaleType = 1
+        private set
+
+    /**
+     * Zoom type (start position).
+     */
+    var zoomType = 1
+        private set
+
+    /**
+     * Initializes the pager.
+     *
+     * @param pager the pager to initialize.
+     */
+    protected fun initializePager(pager: Pager) {
+        adapter = PagerReaderAdapter(childFragmentManager)
+
+        this.pager = pager.apply {
+            setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))
+            setOffscreenPageLimit(1)
+            setId(R.id.view_pager)
+            setOnChapterBoundariesOutListener(object : OnChapterBoundariesOutListener {
+                override fun onFirstPageOutEvent() {
+                    readerActivity.requestPreviousChapter()
+                }
+
+                override fun onLastPageOutEvent() {
+                    readerActivity.requestNextChapter()
+                }
+            })
+            setOnPageChangeListener { onPageChanged(it) }
+        }
+        pager.adapter = adapter
+
+        subscriptions = CompositeSubscription().apply {
+            val preferences = readerActivity.preferences
+
+            add(preferences.imageDecoder()
+                    .asObservable()
+                    .doOnNext { setDecoderClass(it) }
+                    .skip(1)
+                    .distinctUntilChanged()
+                    .subscribe { refreshAdapter() })
+
+            add(preferences.zoomStart()
+                    .asObservable()
+                    .doOnNext { setZoomStart(it) }
+                    .skip(1)
+                    .distinctUntilChanged()
+                    .subscribe { refreshAdapter() })
+
+            add(preferences.imageScaleType()
+                    .asObservable()
+                    .doOnNext { scaleType = it }
+                    .skip(1)
+                    .distinctUntilChanged()
+                    .subscribe { refreshAdapter() })
+
+            add(preferences.enableTransitions()
+                    .asObservable()
+                    .subscribe { transitions = it })
+        }
+
+        setPagesOnAdapter()
+    }
+
+    override fun onDestroyView() {
+        pager.clearOnPageChangeListeners()
+        subscriptions?.unsubscribe()
+        super.onDestroyView()
+    }
+
+    /**
+     * Creates the gesture detector for the pager.
+     *
+     * @return a gesture detector.
+     */
+    protected fun createGestureDetector(): GestureDetector {
+        return GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
+            override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+                val positionX = e.x
+
+                if (positionX < pager.width * LEFT_REGION) {
+                    onLeftSideTap()
+                } else if (positionX > pager.width * RIGHT_REGION) {
+                    onRightSideTap()
+                } else {
+                    readerActivity.onCenterSingleTap()
+                }
+                return true
+            }
+        })
+    }
+
+    /**
+     * Called when a new chapter is set in [BaseReader].
+     *
+     * @param chapter the chapter set.
+     * @param currentPage the initial page to display.
+     */
+    override fun onChapterSet(chapter: Chapter, currentPage: Page) {
+        this.currentPage = getPageIndex(currentPage) // we might have a new page object
+
+        // Make sure the view is already initialized.
+        if (view != null) {
+            setPagesOnAdapter()
+        }
+    }
+
+    /**
+     * Called when a chapter is appended in [BaseReader].
+     *
+     * @param chapter the chapter appended.
+     */
+    override fun onChapterAppended(chapter: Chapter) {
+        // Make sure the view is already initialized.
+        if (view != null) {
+            adapter.pages = pages
+        }
+    }
+
+    /**
+     * Sets the pages on the adapter.
+     */
+    protected fun setPagesOnAdapter() {
+        if (pages.isNotEmpty()) {
+            adapter.pages = pages
+            setActivePage(currentPage)
+            updatePageNumber()
+        }
+    }
+
+    /**
+     * Sets the active page.
+     *
+     * @param pageNumber the index of the page from [pages].
+     */
+    override fun setActivePage(pageNumber: Int) {
+        pager.setCurrentItem(pageNumber, false)
+    }
+
+    /**
+     * Refresh the adapter.
+     */
+    private fun refreshAdapter() {
+        pager.adapter = adapter
+        pager.setCurrentItem(currentPage, false)
+    }
+
+    /**
+     * Called when the left side of the screen was clicked.
+     */
+    protected open fun onLeftSideTap() {
+        moveToPrevious()
+    }
+
+    /**
+     * Called when the right side of the screen was clicked.
+     */
+    protected open fun onRightSideTap() {
+        moveToNext()
+    }
+
+    /**
+     * Moves to the next page or requests the next chapter if it's the last one.
+     */
+    override fun moveToNext() {
+        if (pager.currentItem != pager.adapter.count - 1) {
+            pager.setCurrentItem(pager.currentItem + 1, transitions)
+        } else {
+            readerActivity.requestNextChapter()
+        }
+    }
+
+    /**
+     * Moves to the previous page or requests the previous chapter if it's the first one.
+     */
+    override fun moveToPrevious() {
+        if (pager.currentItem != 0) {
+            pager.setCurrentItem(pager.currentItem - 1, transitions)
+        } else {
+            readerActivity.requestPreviousChapter()
+        }
+    }
+
+    /**
+     * Sets the zoom start position.
+     *
+     * @param zoomStart the value stored in preferences.
+     */
+    private fun setZoomStart(zoomStart: Int) {
+        if (zoomStart == ALIGN_AUTO) {
+            if (this is LeftToRightReader)
+                setZoomStart(ALIGN_LEFT)
+            else if (this is RightToLeftReader)
+                setZoomStart(ALIGN_RIGHT)
+            else
+                setZoomStart(ALIGN_CENTER)
+        } else {
+            zoomType = zoomStart
+        }
+    }
+
+}

+ 0 - 60
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderAdapter.java

@@ -1,60 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager;
-
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentStatePagerAdapter;
-import android.view.ViewGroup;
-
-import java.util.List;
-
-import eu.kanade.tachiyomi.data.source.model.Page;
-
-public class PagerReaderAdapter extends FragmentStatePagerAdapter {
-
-    private List<Page> pages;
-
-    public PagerReaderAdapter(FragmentManager fragmentManager) {
-        super(fragmentManager);
-    }
-
-    @Override
-    public int getCount() {
-        return pages == null ? 0 : pages.size();
-    }
-
-    @Override
-    public Fragment getItem(int position) {
-        return PagerReaderFragment.newInstance();
-    }
-
-    @Override
-    public Object instantiateItem(ViewGroup container, int position) {
-        PagerReaderFragment f = (PagerReaderFragment) super.instantiateItem(container, position);
-        f.setPage(pages.get(position));
-        f.setPosition(position);
-        return f;
-    }
-
-    public List<Page> getPages() {
-        return pages;
-    }
-
-    public void setPages(List<Page> pages) {
-        this.pages = pages;
-        notifyDataSetChanged();
-    }
-
-    @Override
-    public int getItemPosition(Object object) {
-        PagerReaderFragment f = (PagerReaderFragment) object;
-        int position = f.getPosition();
-        if (position >= 0 && position < getCount()) {
-            if (pages.get(position) == f.getPage()) {
-                return POSITION_UNCHANGED;
-            } else {
-                return POSITION_NONE;
-            }
-        }
-        return super.getItemPosition(object);
-    }
-}

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

@@ -0,0 +1,78 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.pager
+
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentManager
+import android.support.v4.app.FragmentStatePagerAdapter
+import android.support.v4.view.PagerAdapter
+import android.view.ViewGroup
+
+import eu.kanade.tachiyomi.data.source.model.Page
+
+/**
+ * Adapter of pages for a ViewPager.
+ *
+ * @param fm the fragment manager.
+ */
+class PagerReaderAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
+
+    /**
+     * Pages stored in the adapter.
+     */
+    var pages: List<Page>? = null
+        set(value) {
+            field = value
+            notifyDataSetChanged()
+        }
+
+    /**
+     * 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
+    }
+
+    /**
+     * Creates a new fragment for the given position when it's called.
+     *
+     * @param position the position to instantiate.
+     * @return a fragment for the given position.
+     */
+    override fun getItem(position: Int): Fragment {
+        return PagerReaderFragment.newInstance()
+    }
+
+    /**
+     * Instantiates a fragment in the given position.
+     *
+     * @param container the parent view.
+     * @param position the position to instantiate.
+     * @return an instance of a fragment for the given position.
+     */
+    override fun instantiateItem(container: ViewGroup, position: Int): Any {
+        val f = super.instantiateItem(container, position) as PagerReaderFragment
+        f.page = pages!![position]
+        f.position = position
+        return f
+    }
+
+    /**
+     * Returns the position of a given item.
+     *
+     * @param obj the item to find its position.
+     * @return the position for the item.
+     */
+    override fun getItemPosition(obj: Any): Int {
+        val f = obj as PagerReaderFragment
+        val position = f.position
+        if (position >= 0 && position < count) {
+            if (pages!![position] === f.page) {
+                return PagerAdapter.POSITION_UNCHANGED
+            } else {
+                return PagerAdapter.POSITION_NONE
+            }
+        }
+        return super.getItemPosition(obj)
+    }
+}

+ 0 - 270
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderFragment.java

@@ -1,270 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager;
-
-import android.graphics.PointF;
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.v4.content.ContextCompat;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import com.davemorrissey.labs.subscaleview.ImageSource;
-import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
-
-import java.io.File;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import butterknife.Bind;
-import butterknife.ButterKnife;
-import eu.kanade.tachiyomi.R;
-import eu.kanade.tachiyomi.data.source.model.Page;
-import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
-import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout;
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader;
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader;
-import rx.Observable;
-import rx.Subscription;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.schedulers.Schedulers;
-import rx.subjects.PublishSubject;
-
-public class PagerReaderFragment extends BaseFragment {
-
-    @Bind(R.id.page_image_view) SubsamplingScaleImageView imageView;
-    @Bind(R.id.progress_container) LinearLayout progressContainer;
-    @Bind(R.id.progress) ProgressBar progressBar;
-    @Bind(R.id.progress_text) TextView progressText;
-    @Bind(R.id.retry_button) Button retryButton;
-
-    private Page page;
-    private Subscription progressSubscription;
-    private Subscription statusSubscription;
-    private int position = -1;
-
-    private int lightGreyColor;
-    private int blackColor;
-
-    public static PagerReaderFragment newInstance() {
-        return new PagerReaderFragment();
-    }
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
-        View view = inflater.inflate(R.layout.item_pager_reader, container, false);
-        ButterKnife.bind(this, view);
-        ReaderActivity activity = getReaderActivity();
-        PagerReader parentFragment = (PagerReader) getParentFragment();
-
-        lightGreyColor = ContextCompat.getColor(getContext(), R.color.light_grey);
-        blackColor = ContextCompat.getColor(getContext(), R.color.primary_text);
-
-        if (activity.getReaderTheme() == ReaderActivity.BLACK_THEME) {
-             progressText.setTextColor(lightGreyColor);
-        }
-
-        if (parentFragment instanceof RightToLeftReader) {
-            view.setRotation(-180);
-        }
-
-        imageView.setParallelLoadingEnabled(true);
-        imageView.setMaxBitmapDimensions(activity.getMaxBitmapSize());
-        imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED);
-        imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE);
-        imageView.setMinimumScaleType(parentFragment.scaleType);
-        imageView.setMinimumDpi(50);
-        imageView.setRegionDecoderClass(parentFragment.getRegionDecoderClass());
-        imageView.setBitmapDecoderClass(parentFragment.getBitmapDecoderClass());
-        imageView.setVerticalScrollingParent(parentFragment instanceof VerticalReader);
-        imageView.setOnTouchListener((v, motionEvent) -> parentFragment.gestureDetector.onTouchEvent(motionEvent));
-        imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
-            @Override
-            public void onReady() {
-                switch (parentFragment.zoomStart) {
-                    case PagerReader.ALIGN_LEFT:
-                        imageView.setScaleAndCenter(imageView.getScale(), new PointF(0, 0));
-                        break;
-                    case PagerReader.ALIGN_RIGHT:
-                        imageView.setScaleAndCenter(imageView.getScale(), new PointF(imageView.getSWidth(), 0));
-                        break;
-                    case PagerReader.ALIGN_CENTER:
-                        PointF center = imageView.getCenter();
-                        center.y = 0;
-                        imageView.setScaleAndCenter(imageView.getScale(), center);
-                        break;
-                }
-            }
-
-            @Override
-            public void onImageLoadError(Exception e) {
-                showImageDecodeError();
-            }
-        });
-
-        retryButton.setOnTouchListener((v, event) -> {
-            if (event.getAction() == MotionEvent.ACTION_UP) {
-                activity.getPresenter().retryPage(page);
-            }
-            return true;
-        });
-
-        observeStatus();
-        return view;
-    }
-
-    @Override
-    public void onDestroyView() {
-        unsubscribeProgress();
-        unsubscribeStatus();
-        imageView.setOnTouchListener(null);
-        imageView.setOnImageEventListener(null);
-        ButterKnife.unbind(this);
-        super.onDestroyView();
-    }
-
-    public void setPage(Page page) {
-        this.page = page;
-
-        // This method can be called before the view is created
-        if (imageView != null) {
-            observeStatus();
-        }
-    }
-
-    public void setPosition(int position) {
-        this.position = position;
-    }
-
-    private void showImage() {
-        if (page == null || page.getImagePath() == null)
-            return;
-
-        File imagePath = new File(page.getImagePath());
-        if (imagePath.exists()) {
-            imageView.setImage(ImageSource.uri(page.getImagePath()));
-            progressContainer.setVisibility(View.GONE);
-        } else {
-            page.setStatus(Page.ERROR);
-        }
-    }
-
-    private void showDownloading() {
-        progressContainer.setVisibility(View.VISIBLE);
-        progressText.setVisibility(View.VISIBLE);
-    }
-
-    private void showLoading() {
-        progressContainer.setVisibility(View.VISIBLE);
-        progressText.setVisibility(View.VISIBLE);
-        progressText.setText(R.string.downloading);
-    }
-
-    private void showError() {
-        progressContainer.setVisibility(View.GONE);
-        retryButton.setVisibility(View.VISIBLE);
-    }
-
-    private void hideError() {
-        retryButton.setVisibility(View.GONE);
-    }
-
-    private void showImageDecodeError() {
-        ViewGroup view = (ViewGroup) getView();
-        if (view == null)
-            return;
-
-        LinearLayout errorLayout = new PageDecodeErrorLayout(getContext(), page,
-                getReaderActivity().getReaderTheme(),
-                () -> getReaderActivity().getPresenter().retryPage(page));
-
-        view.addView(errorLayout);
-    }
-
-    private void processStatus(int status) {
-        switch (status) {
-            case Page.QUEUE:
-                hideError();
-                break;
-            case Page.LOAD_PAGE:
-                showLoading();
-                break;
-            case Page.DOWNLOAD_IMAGE:
-                observeProgress();
-                showDownloading();
-                break;
-            case Page.READY:
-                showImage();
-                unsubscribeProgress();
-                break;
-            case Page.ERROR:
-                showError();
-                unsubscribeProgress();
-                break;
-        }
-    }
-
-    private void observeStatus() {
-        if (page == null || statusSubscription != null)
-            return;
-
-        PublishSubject<Integer> statusSubject = PublishSubject.create();
-        page.setStatusSubject(statusSubject);
-
-        statusSubscription = statusSubject
-                .startWith(page.getStatus())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(this::processStatus);
-    }
-
-    private void observeProgress() {
-        if (progressSubscription != null)
-            return;
-
-        final AtomicInteger currentValue = new AtomicInteger(-1);
-
-        progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS, Schedulers.newThread())
-                .onBackpressureLatest()
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(tick -> {
-                    // Refresh UI only if progress change
-                    if (page.getProgress() != currentValue.get()) {
-                        currentValue.set(page.getProgress());
-                        progressText.setText(getString(R.string.download_progress, page.getProgress()));
-                    }
-                });
-    }
-
-    private void unsubscribeStatus() {
-        if (statusSubscription != null) {
-            page.setStatusSubject(null);
-            statusSubscription.unsubscribe();
-            statusSubscription = null;
-        }
-    }
-
-    private void unsubscribeProgress() {
-        if (progressSubscription != null) {
-            progressSubscription.unsubscribe();
-            progressSubscription = null;
-        }
-    }
-
-    public Page getPage() {
-        return page;
-    }
-
-    public int getPosition() {
-        return position;
-    }
-
-    private ReaderActivity getReaderActivity() {
-        return (ReaderActivity) getActivity();
-    }
-
-}

+ 294 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerReaderFragment.kt

@@ -0,0 +1,294 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.pager
+
+import android.graphics.PointF
+import android.os.Bundle
+import android.support.v4.content.ContextCompat
+import android.view.LayoutInflater
+import android.view.MotionEvent
+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.base.fragment.BaseFragment
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.ui.reader.viewer.base.PageDecodeErrorLayout
+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.*
+import kotlinx.android.synthetic.main.item_pager_reader.*
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import rx.subjects.PublishSubject
+import java.io.File
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Fragment for a single page of the ViewPager reader.
+ * All the elements from the layout file "item_pager_reader" are available in this class.
+ */
+class PagerReaderFragment : BaseFragment() {
+
+    companion object {
+        /**
+         * Creates a new instance of this fragment.
+         *
+         * @return a new instance of [PagerReaderFragment].
+         */
+        fun newInstance(): PagerReaderFragment {
+            return PagerReaderFragment()
+        }
+    }
+
+    /**
+     * Page of a chapter.
+     */
+    var page: Page? = null
+        set(value) {
+            field = value
+            // Observe status if the view is initialized
+            if (view != null) {
+                observeStatus()
+            }
+        }
+
+    /**
+     * Position of the fragment in the adapter.
+     */
+    var position = -1
+
+    /**
+     * Subscription for progress changes of the page.
+     */
+    private var progressSubscription: Subscription? = null
+
+    /**
+     * Subscription for status changes of the page.
+     */
+    private var statusSubscription: Subscription? = null
+
+    /**
+     * Text color for black theme.
+     */
+    private val lightGreyColor by lazy { ContextCompat.getColor(context, R.color.light_grey) }
+
+    /**
+     * Text color for white theme.
+     */
+    private val blackColor by lazy { ContextCompat.getColor(context, R.color.primary_text) }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
+        return inflater.inflate(R.layout.item_pager_reader, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedState: Bundle?) {
+        if (readerActivity.readerTheme == ReaderActivity.BLACK_THEME) {
+            progress_text.setTextColor(lightGreyColor)
+        }
+
+        if (pagerReader is RightToLeftReader) {
+            view.rotation = -180f
+        }
+
+        with(image_view) {
+            setParallelLoadingEnabled(true)
+            setMaxBitmapDimensions(readerActivity.maxBitmapSize)
+            setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
+            setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
+            setMinimumScaleType(pagerReader.scaleType)
+            setMinimumDpi(50)
+            setRegionDecoderClass(pagerReader.regionDecoderClass)
+            setBitmapDecoderClass(pagerReader.bitmapDecoderClass)
+            setVerticalScrollingParent(pagerReader is VerticalReader)
+            setOnTouchListener { v, motionEvent -> pagerReader.gestureDetector.onTouchEvent(motionEvent) }
+            setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
+                override fun onReady() {
+                    when (pagerReader.zoomType) {
+                        PagerReader.ALIGN_LEFT -> setScaleAndCenter(scale, PointF(0f, 0f))
+                        PagerReader.ALIGN_RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0f))
+                        PagerReader.ALIGN_CENTER -> {
+                            val newCenter = center
+                            newCenter.y = 0f
+                            setScaleAndCenter(scale, newCenter)
+                        }
+                    }
+                }
+
+                override fun onImageLoadError(e: Exception) {
+                    onImageDecodeError()
+                }
+            })
+        }
+
+        retry_button.setOnTouchListener { v, event ->
+            if (event.action == MotionEvent.ACTION_UP) {
+                readerActivity.presenter.retryPage(page)
+            }
+            true
+        }
+
+        observeStatus()
+    }
+
+    override fun onDestroyView() {
+        unsubscribeProgress()
+        unsubscribeStatus()
+        image_view.setOnTouchListener(null)
+        image_view.setOnImageEventListener(null)
+        super.onDestroyView()
+    }
+
+    /**
+     * Observes the status of the page and notify the changes.
+     *
+     * @see processStatus
+     */
+    private fun observeStatus() {
+        page?.let { page ->
+            val statusSubject = PublishSubject.create<Int>()
+            page.setStatusSubject(statusSubject)
+
+            statusSubscription?.unsubscribe()
+            statusSubscription = statusSubject.startWith(page.status)
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe { processStatus(it) }
+        }
+    }
+
+    /**
+     * Observes the progress of the page and updates view.
+     */
+    private fun observeProgress() {
+        val currentValue = AtomicInteger(-1)
+
+        progressSubscription?.unsubscribe()
+        progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS, Schedulers.newThread())
+                .onBackpressureLatest()
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe {
+                    // Refresh UI only if progress change
+                    if (page?.progress != currentValue.get()) {
+                        currentValue.set(page?.progress ?: 0)
+                        progress_text.text = getString(R.string.download_progress, currentValue.get())
+                    }
+                }
+    }
+
+    /**
+     * Called when the status of the page changes.
+     *
+     * @param status the new status of the page.
+     */
+    private fun processStatus(status: Int) {
+        when (status) {
+            Page.QUEUE -> hideError()
+            Page.LOAD_PAGE -> onLoading()
+            Page.DOWNLOAD_IMAGE -> {
+                observeProgress()
+                onDownloading()
+            }
+            Page.READY -> {
+                onReady()
+                unsubscribeProgress()
+            }
+            Page.ERROR -> {
+                onError()
+                unsubscribeProgress()
+            }
+        }
+    }
+
+    /**
+     * Unsubscribes from the status subscription.
+     */
+    private fun unsubscribeStatus() {
+        page?.setStatusSubject(null)
+        statusSubscription?.unsubscribe()
+        statusSubscription = null
+    }
+
+    /**
+     * Unsubscribes from the progress subscription.
+     */
+    private fun unsubscribeProgress() {
+        progressSubscription?.unsubscribe()
+        progressSubscription = null
+    }
+
+    /**
+     * Called when the page is loading.
+     */
+    private fun onLoading() {
+        progress_container.visibility = View.VISIBLE
+        progress_text.visibility = View.VISIBLE
+        progress_text.setText(R.string.downloading)
+    }
+
+    /**
+     * Called when the page is downloading.
+     */
+    private fun onDownloading() {
+        progress_container.visibility = View.VISIBLE
+        progress_text.visibility = View.VISIBLE
+    }
+
+    /**
+     * 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
+            }
+        }
+    }
+
+    /**
+     * Called when the page has an error.
+     */
+    private fun onError() {
+        progress_container.visibility = View.GONE
+        retry_button.visibility = View.VISIBLE
+    }
+
+    /**
+     * Hides the error layout.
+     */
+    private fun hideError() {
+        retry_button.visibility = View.GONE
+    }
+
+    /**
+     * Called when an image fails to decode.
+     */
+    private fun onImageDecodeError() {
+        val view = view as? ViewGroup ?: return
+
+        page?.let { page ->
+            val errorLayout = PageDecodeErrorLayout(context, page, readerActivity.readerTheme,
+                    { readerActivity.presenter.retryPage(page) })
+
+            view.addView(errorLayout)
+        }
+    }
+
+    /**
+     * Property to get the reader activity.
+     */
+    private val readerActivity: ReaderActivity
+        get() = activity as ReaderActivity
+
+    /**
+     * Property to get the pager reader.
+     */
+    private val pagerReader: PagerReader
+        get() = parentFragment as PagerReader
+
+}

+ 0 - 87
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/HorizontalPager.java

@@ -1,87 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
-
-import android.content.Context;
-import android.support.v4.view.ViewPager;
-import android.view.MotionEvent;
-
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.OnChapterBoundariesOutListener;
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager;
-import rx.functions.Action1;
-
-public class HorizontalPager extends ViewPager implements Pager {
-
-    private OnChapterBoundariesOutListener onChapterBoundariesOutListener;
-
-    private static final float SWIPE_TOLERANCE = 0.25f;
-    private float startDragX;
-
-    public HorizontalPager(Context context) {
-        super(context);
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        try {
-            if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
-                if (getCurrentItem() == 0 || getCurrentItem() == getAdapter().getCount() - 1) {
-                    startDragX = ev.getX();
-                }
-            }
-
-            return super.onInterceptTouchEvent(ev);
-        } catch (IllegalArgumentException e) {
-            return false;
-        }
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        try {
-            if (onChapterBoundariesOutListener != null) {
-                if (getCurrentItem() == 0) {
-                    if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
-                        float displacement = ev.getX() - startDragX;
-
-                        if (ev.getX() > startDragX && displacement > getWidth() * SWIPE_TOLERANCE) {
-                            onChapterBoundariesOutListener.onFirstPageOutEvent();
-                            return true;
-                        }
-
-                        startDragX = 0;
-                    }
-                } else if (getCurrentItem() == getAdapter().getCount() - 1) {
-                    if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
-                        float displacement = startDragX - ev.getX();
-
-                        if (ev.getX() < startDragX && displacement > getWidth() * SWIPE_TOLERANCE) {
-                            onChapterBoundariesOutListener.onLastPageOutEvent();
-                            return true;
-                        }
-
-                        startDragX = 0;
-                    }
-                }
-            }
-
-            return super.onTouchEvent(ev);
-        } catch (IllegalArgumentException e) {
-            return false;
-        }
-    }
-
-    @Override
-    public void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener) {
-        onChapterBoundariesOutListener = listener;
-    }
-
-    @Override
-    public void setOnPageChangeListener(Action1<Integer> function) {
-        addOnPageChangeListener(new SimpleOnPageChangeListener() {
-            @Override
-            public void onPageSelected(int position) {
-                function.call(position);
-            }
-        });
-    }
-
-}

+ 86 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/HorizontalPager.kt

@@ -0,0 +1,86 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal
+
+import android.content.Context
+import android.support.v4.view.ViewPager
+import android.view.MotionEvent
+import eu.kanade.tachiyomi.ui.reader.viewer.pager.OnChapterBoundariesOutListener
+import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager
+import rx.functions.Action1
+
+/**
+ * Implementation of a [ViewPager] to add custom behavior on touch events.
+ */
+class HorizontalPager(context: Context) : ViewPager(context), Pager {
+
+    companion object {
+
+        const val SWIPE_TOLERANCE = 0.25f
+    }
+
+    private var onChapterBoundariesOutListener: OnChapterBoundariesOutListener? = null
+
+    private var startDragX: Float = 0f
+
+    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+        try {
+            if (ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_DOWN) {
+                if (currentItem == 0 || currentItem == adapter.count - 1) {
+                    startDragX = ev.x
+                }
+            }
+
+            return super.onInterceptTouchEvent(ev)
+        } catch (e: IllegalArgumentException) {
+            return false
+        }
+
+    }
+
+    override fun onTouchEvent(ev: MotionEvent): Boolean {
+        try {
+            onChapterBoundariesOutListener?.let { listener ->
+                if (currentItem == 0) {
+                    if (ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_UP) {
+                        val displacement = ev.x - startDragX
+
+                        if (ev.x > startDragX && displacement > width * SWIPE_TOLERANCE) {
+                            listener.onFirstPageOutEvent()
+                            return true
+                        }
+
+                        startDragX = 0f
+                    }
+                } else if (currentItem == adapter.count - 1) {
+                    if (ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_UP) {
+                        val displacement = startDragX - ev.x
+
+                        if (ev.x < startDragX && displacement > width * SWIPE_TOLERANCE) {
+                            listener.onLastPageOutEvent()
+                            return true
+                        }
+
+                        startDragX = 0f
+                    }
+                }
+            }
+
+            return super.onTouchEvent(ev)
+        } catch (e: IllegalArgumentException) {
+            return false
+        }
+
+    }
+
+    override fun setOnChapterBoundariesOutListener(listener: OnChapterBoundariesOutListener) {
+        onChapterBoundariesOutListener = listener
+    }
+
+    override fun setOnPageChangeListener(func: Action1<Int>) {
+        addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
+            override fun onPageSelected(position: Int) {
+                func.call(position)
+            }
+        })
+    }
+
+}

+ 0 - 19
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/LeftToRightReader.java

@@ -1,19 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader;
-
-public class LeftToRightReader extends PagerReader {
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
-        HorizontalPager pager = new HorizontalPager(getActivity());
-        initializePager(pager);
-        return pager;
-    }
-
-}

+ 19 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/LeftToRightReader.kt

@@ -0,0 +1,19 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+
+import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader
+
+/**
+ * Left to Right reader.
+ */
+class LeftToRightReader : PagerReader() {
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
+        return HorizontalPager(activity).apply { initializePager(this) }
+    }
+
+}

+ 0 - 30
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/RightToLeftReader.java

@@ -1,30 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader;
-
-public class RightToLeftReader extends PagerReader {
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
-        HorizontalPager pager = new HorizontalPager(getActivity());
-        pager.setRotation(180);
-        initializePager(pager);
-        return pager;
-    }
-
-    @Override
-    protected void onLeftSideTap() {
-        moveToNext();
-    }
-
-    @Override
-    protected void onRightSideTap() {
-        moveToPrevious();
-    }
-
-}

+ 30 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/horizontal/RightToLeftReader.kt

@@ -0,0 +1,30 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+
+import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader
+
+/**
+ * Right to Left reader.
+ */
+class RightToLeftReader : PagerReader() {
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
+        return HorizontalPager(activity).apply {
+            rotation = 180f
+            initializePager(this)
+        }
+    }
+
+    override fun onLeftSideTap() {
+        moveToNext()
+    }
+
+    override fun onRightSideTap() {
+        moveToPrevious()
+    }
+
+}

+ 0 - 86
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalPager.java

@@ -1,86 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical;
-
-import android.content.Context;
-import android.view.MotionEvent;
-
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.OnChapterBoundariesOutListener;
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager;
-import rx.functions.Action1;
-
-public class VerticalPager extends VerticalViewPagerImpl implements Pager {
-
-    private OnChapterBoundariesOutListener onChapterBoundariesOutListener;
-
-    private static final float SWIPE_TOLERANCE = 0.25f;
-    private float startDragY;
-
-    public VerticalPager(Context context) {
-        super(context);
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        try {
-            if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
-                if (getCurrentItem() == 0 || getCurrentItem() == getAdapter().getCount() - 1) {
-                    startDragY = ev.getY();
-                }
-            }
-
-            return super.onInterceptTouchEvent(ev);
-        } catch (IllegalArgumentException e) {
-            return false;
-        }
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        try {
-            if (onChapterBoundariesOutListener != null) {
-                if (getCurrentItem() == 0) {
-                    if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
-                        float displacement = ev.getY() - startDragY;
-
-                        if (ev.getY() > startDragY && displacement > getHeight() * SWIPE_TOLERANCE) {
-                            onChapterBoundariesOutListener.onFirstPageOutEvent();
-                            return true;
-                        }
-
-                        startDragY = 0;
-                    }
-                } else if (getCurrentItem() == getAdapter().getCount() - 1) {
-                    if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
-                        float displacement = startDragY - ev.getY();
-
-                        if (ev.getY() < startDragY && displacement > getHeight() * SWIPE_TOLERANCE) {
-                            onChapterBoundariesOutListener.onLastPageOutEvent();
-                            return true;
-                        }
-
-                        startDragY = 0;
-                    }
-                }
-            }
-
-            return super.onTouchEvent(ev);
-        } catch (IllegalArgumentException e) {
-            return false;
-        }
-    }
-
-    @Override
-    public void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener) {
-        onChapterBoundariesOutListener = listener;
-    }
-
-    @Override
-    public void setOnPageChangeListener(Action1<Integer> function) {
-        addOnPageChangeListener(new SimpleOnPageChangeListener() {
-            @Override
-            public void onPageSelected(int position) {
-                function.call(position);
-            }
-        });
-    }
-    
-}

+ 84 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalPager.kt

@@ -0,0 +1,84 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical
+
+import android.content.Context
+import android.view.MotionEvent
+import eu.kanade.tachiyomi.ui.reader.viewer.pager.OnChapterBoundariesOutListener
+import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager
+import rx.functions.Action1
+
+/**
+ * Implementation of a [VerticalViewPagerImpl] to add custom behavior on touch events.
+ */
+class VerticalPager(context: Context) : VerticalViewPagerImpl(context), Pager {
+
+    private var onChapterBoundariesOutListener: OnChapterBoundariesOutListener? = null
+    private var startDragY: Float = 0.toFloat()
+
+    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+        try {
+            if (ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_DOWN) {
+                if (currentItem == 0 || currentItem == adapter.count - 1) {
+                    startDragY = ev.y
+                }
+            }
+
+            return super.onInterceptTouchEvent(ev)
+        } catch (e: IllegalArgumentException) {
+            return false
+        }
+
+    }
+
+    override fun onTouchEvent(ev: MotionEvent): Boolean {
+        try {
+            onChapterBoundariesOutListener?.let { listener ->
+                if (currentItem == 0) {
+                    if (ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_UP) {
+                        val displacement = ev.y - startDragY
+
+                        if (ev.y > startDragY && displacement > height * SWIPE_TOLERANCE) {
+                            listener.onFirstPageOutEvent()
+                            return true
+                        }
+
+                        startDragY = 0f
+                    }
+                } else if (currentItem == adapter.count - 1) {
+                    if (ev.action and MotionEvent.ACTION_MASK == MotionEvent.ACTION_UP) {
+                        val displacement = startDragY - ev.y
+
+                        if (ev.y < startDragY && displacement > height * SWIPE_TOLERANCE) {
+                            listener.onLastPageOutEvent()
+                            return true
+                        }
+
+                        startDragY = 0f
+                    }
+                }
+            }
+
+            return super.onTouchEvent(ev)
+        } catch (e: IllegalArgumentException) {
+            return false
+        }
+
+    }
+
+    override fun setOnChapterBoundariesOutListener(listener: OnChapterBoundariesOutListener) {
+        onChapterBoundariesOutListener = listener
+    }
+
+    override fun setOnPageChangeListener(func: Action1<Int>) {
+        addOnPageChangeListener(object : VerticalViewPagerImpl.SimpleOnPageChangeListener() {
+            override fun onPageSelected(position: Int) {
+                func.call(position)
+            }
+        })
+    }
+
+    companion object {
+
+        private val SWIPE_TOLERANCE = 0.25f
+    }
+
+}

+ 0 - 19
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalReader.java

@@ -1,19 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader;
-
-public class VerticalReader extends PagerReader {
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
-        VerticalPager pager = new VerticalPager(getActivity());
-        initializePager(pager);
-        return pager;
-    }
-
-}

+ 19 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/vertical/VerticalReader.kt

@@ -0,0 +1,19 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+
+import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader
+
+/**
+ * Vertical reader.
+ */
+class VerticalReader : PagerReader() {
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
+        return VerticalPager(activity).apply { initializePager(this) }
+    }
+
+}

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

@@ -1,72 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.webtoon;
-
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import eu.kanade.tachiyomi.R;
-import eu.kanade.tachiyomi.data.source.model.Page;
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
-
-public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
-
-    private WebtoonReader fragment;
-    private List<Page> pages;
-    private View.OnTouchListener touchListener;
-
-    public WebtoonAdapter(WebtoonReader fragment) {
-        this.fragment = fragment;
-        pages = new ArrayList<>();
-        touchListener = (v, event) -> fragment.gestureDetector.onTouchEvent(event);
-    }
-
-    public Page getItem(int position) {
-        return pages.get(position);
-    }
-
-    @Override
-    public WebtoonHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        LayoutInflater inflater = fragment.getActivity().getLayoutInflater();
-        View v = inflater.inflate(R.layout.item_webtoon_reader, parent, false);
-        return new WebtoonHolder(v, this, touchListener);
-    }
-
-    @Override
-    public void onBindViewHolder(WebtoonHolder holder, int position) {
-        final Page page = getItem(position);
-        holder.onSetValues(page);
-    }
-
-    @Override
-    public int getItemCount() {
-        return pages.size();
-    }
-
-    public void setPages(List<Page> pages) {
-        this.pages = pages;
-    }
-
-    public void clear() {
-        if (pages != null) {
-            pages.clear();
-            notifyDataSetChanged();
-        }
-    }
-
-    public void retryPage(Page page) {
-        fragment.getReaderActivity().getPresenter().retryPage(page);
-    }
-
-    public WebtoonReader getReader() {
-        return fragment;
-    }
-
-    public ReaderActivity getReaderActivity() {
-        return (ReaderActivity) fragment.getActivity();
-    }
-
-}

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

@@ -0,0 +1,78 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
+
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.view.ViewGroup
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.util.inflate
+
+/**
+ * Adapter of pages for a RecyclerView.
+ *
+ * @param fragment the fragment containing this adapter.
+ */
+class WebtoonAdapter(val fragment: WebtoonReader) : RecyclerView.Adapter<WebtoonHolder>() {
+
+    /**
+     * Pages stored in the adapter.
+     */
+    var pages: List<Page>? = null
+
+    /**
+     * Touch listener for images in holders.
+     */
+    val touchListener = View.OnTouchListener { v, ev -> fragment.gestureDetector.onTouchEvent(ev) }
+
+    /**
+     * Returns the number of pages.
+     *
+     * @return the number of pages or 0 if the list is null.
+     */
+    override fun getItemCount(): Int {
+        return pages?.size ?: 0
+    }
+
+    /**
+     * Returns a page given the position.
+     *
+     * @param position the position of the page.
+     * @return the page.
+     */
+    fun getItem(position: Int): Page {
+        return pages!![position]
+    }
+
+    /**
+     * Creates a new view holder.
+     *
+     * @param parent the parent view.
+     * @param viewType the type of the holder.
+     * @return a new view holder for a manga.
+     */
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WebtoonHolder {
+        val v = parent.inflate(R.layout.item_webtoon_reader)
+        return WebtoonHolder(v, this)
+    }
+
+    /**
+     * Binds a holder with a new position.
+     *
+     * @param holder the holder to bind.
+     * @param position the position to bind.
+     */
+    override fun onBindViewHolder(holder: WebtoonHolder, position: Int) {
+        val page = getItem(position)
+        holder.onSetValues(page)
+    }
+
+    /**
+     * Recycles the view holder.
+     *
+     * @param holder the holder to recycle.
+     */
+    override fun onViewRecycled(holder: WebtoonHolder) {
+        holder.unsubscribeStatus()
+    }
+
+}

+ 0 - 135
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonHolder.java

@@ -1,135 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.webtoon;
-
-import android.support.v7.widget.RecyclerView;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ProgressBar;
-
-import com.davemorrissey.labs.subscaleview.ImageSource;
-import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
-
-import java.io.File;
-
-import butterknife.Bind;
-import butterknife.ButterKnife;
-import eu.kanade.tachiyomi.R;
-import eu.kanade.tachiyomi.data.source.model.Page;
-
-public class WebtoonHolder extends RecyclerView.ViewHolder {
-
-    @Bind(R.id.page_image_view) SubsamplingScaleImageView imageView;
-    @Bind(R.id.frame_container) ViewGroup container;
-    @Bind(R.id.progress) ProgressBar progressBar;
-    @Bind(R.id.retry_button) Button retryButton;
-
-    private Page page;
-    private WebtoonAdapter adapter;
-
-    public WebtoonHolder(View view, WebtoonAdapter adapter, View.OnTouchListener touchListener) {
-        super(view);
-        this.adapter = adapter;
-        ButterKnife.bind(this, view);
-
-        imageView.setParallelLoadingEnabled(true);
-        imageView.setMaxBitmapDimensions(adapter.getReaderActivity().getMaxBitmapSize());
-        imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED);
-        imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE);
-        imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH);
-        imageView.setMaxScale(10);
-        imageView.setRegionDecoderClass(adapter.getReader().getRegionDecoderClass());
-        imageView.setBitmapDecoderClass(adapter.getReader().getBitmapDecoderClass());
-        imageView.setVerticalScrollingParent(true);
-        imageView.setOnTouchListener(touchListener);
-        imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
-            @Override
-            public void onImageLoaded() {
-                // When the image is loaded, reset the minimum height to avoid gaps
-                container.setMinimumHeight(0);
-            }
-        });
-
-        // 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.
-        container.setMinimumHeight(view.getResources().getDisplayMetrics().heightPixels * 2);
-
-        // Leave some space between progress bars
-        progressBar.setMinimumHeight(300);
-
-        container.setOnTouchListener(touchListener);
-        retryButton.setOnTouchListener((v, event) -> {
-            if (event.getAction() == MotionEvent.ACTION_UP) {
-                adapter.retryPage(page);
-            }
-            return true;
-        });
-    }
-
-    public void onSetValues(Page page) {
-        this.page = page;
-        switch (page.getStatus()) {
-            case Page.QUEUE:
-                onQueue();
-                break;
-            case Page.LOAD_PAGE:
-                onLoading();
-                break;
-            case Page.DOWNLOAD_IMAGE:
-                onLoading();
-                break;
-            case Page.READY:
-                onReady();
-                break;
-            case Page.ERROR:
-                onError();
-                break;
-        }
-    }
-
-    private void onLoading() {
-        setErrorButtonVisible(false);
-        setImageVisible(false);
-        setProgressVisible(true);
-    }
-
-    private void onReady() {
-        setErrorButtonVisible(false);
-        setProgressVisible(false);
-        setImageVisible(true);
-
-        File imagePath = new File(page.getImagePath());
-        if (imagePath.exists()) {
-            imageView.setImage(ImageSource.uri(page.getImagePath()));
-        } else {
-            page.setStatus(Page.ERROR);
-            onError();
-        }
-    }
-
-    private void onError() {
-        setImageVisible(false);
-        setProgressVisible(false);
-        setErrorButtonVisible(true);
-    }
-
-    private void onQueue() {
-        setImageVisible(false);
-        setErrorButtonVisible(false);
-        setProgressVisible(false);
-    }
-
-    private void setProgressVisible(boolean visible) {
-        progressBar.setVisibility(visible ? View.VISIBLE : View.GONE);
-    }
-
-    private void setImageVisible(boolean visible) {
-        imageView.setVisibility(visible ? View.VISIBLE : View.GONE);
-    }
-
-    private void setErrorButtonVisible(boolean visible) {
-        retryButton.setVisibility(visible ? View.VISIBLE : View.GONE);
-    }
-}

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

@@ -0,0 +1,240 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
+
+import android.support.v7.widget.RecyclerView
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import com.davemorrissey.labs.subscaleview.ImageSource
+import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
+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.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.subjects.PublishSubject
+import java.io.File
+
+/**
+ * Holder for webtoon reader for a single page of a chapter.
+ * All the elements from the layout file "item_webtoon_reader" are available in this class.
+ *
+ * @param view the inflated view for this holder.
+ * @param adapter the adapter handling this holder.
+ * @constructor creates a new webtoon holder.
+ */
+class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter) :
+        RecyclerView.ViewHolder(view) {
+
+    /**
+     * Page of a chapter.
+     */
+    private var page: Page? = null
+
+    /**
+     * Subscription for status changes of the page.
+     */
+    private var statusSubscription: Subscription? = null
+
+    /**
+     * Layout of decode error.
+     */
+    private var decodeErrorLayout: PageDecodeErrorLayout? = null
+
+    init {
+        with(view.image_view) {
+            setParallelLoadingEnabled(true)
+            setMaxBitmapDimensions(readerActivity.maxBitmapSize)
+            setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED)
+            setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
+            setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH)
+            maxScale = 10f
+            setRegionDecoderClass(webtoonReader.regionDecoderClass)
+            setBitmapDecoderClass(webtoonReader.bitmapDecoderClass)
+            setVerticalScrollingParent(true)
+            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 = 0
+                }
+
+                override fun onImageLoadError(e: Exception) {
+                    onImageDecodeError()
+                }
+            })
+        }
+
+        // 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
+
+        // Leave some space between progress bars
+        view.progress.minimumHeight = 300
+
+        view.frame_container.setOnTouchListener(adapter.touchListener)
+        view.retry_button.setOnTouchListener { v, event ->
+            if (event.action == MotionEvent.ACTION_UP) {
+                readerActivity.presenter.retryPage(page)
+            }
+            true
+        }
+    }
+
+    /**
+     * Method called from [WebtoonAdapter.onBindViewHolder]. It updates the data for this
+     * holder with the given page.
+     *
+     * @param page the page to bind.
+     */
+    fun onSetValues(page: Page) {
+        decodeErrorLayout?.let {
+            (view as ViewGroup).removeView(it)
+            decodeErrorLayout = null
+        }
+
+        this.page = page
+        observeStatus()
+    }
+
+    /**
+     * Observes the status of the page and notify the changes.
+     *
+     * @see processStatus
+     */
+    private fun observeStatus() {
+        page?.let { page ->
+            val statusSubject = PublishSubject.create<Int>()
+            page.setStatusSubject(statusSubject)
+
+            statusSubscription?.unsubscribe()
+            statusSubscription = statusSubject.startWith(page.status)
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .subscribe { processStatus(it) }
+
+            webtoonReader.subscriptions.add(statusSubscription)
+        }
+    }
+
+    /**
+     * Called when the status of the page changes.
+     *
+     * @param status the new status of the page.
+     */
+    private fun processStatus(status: Int) {
+        when (status) {
+            Page.QUEUE -> onQueue()
+            Page.LOAD_PAGE -> onLoading()
+            Page.DOWNLOAD_IMAGE -> onLoading()
+            Page.READY -> onReady()
+            Page.ERROR -> onError()
+        }
+    }
+
+    /**
+     * Unsubscribes from the status subscription.
+     */
+    fun unsubscribeStatus() {
+        statusSubscription?.unsubscribe()
+        statusSubscription = null
+    }
+
+    /**
+     * Called when the page is loading.
+     */
+    private fun onLoading() {
+        setRetryButtonVisible(false)
+        setImageVisible(false)
+        setProgressVisible(true)
+    }
+
+    /**
+     * Called when the page is ready.
+     */
+    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
+            }
+        }
+    }
+
+    /**
+     * Called when the page has an error.
+     */
+    private fun onError() {
+        setImageVisible(false)
+        setProgressVisible(false)
+        setRetryButtonVisible(true)
+    }
+
+    /**
+     * Called when the page is queued.
+     */
+    private fun onQueue() {
+        setImageVisible(false)
+        setRetryButtonVisible(false)
+        setProgressVisible(false)
+    }
+
+    /**
+     * Called when the image fails to decode.
+     */
+    private fun onImageDecodeError() {
+        page?.let { page ->
+            decodeErrorLayout = PageDecodeErrorLayout(view.context, page, readerActivity.readerTheme,
+                    { readerActivity.presenter.retryPage(page) })
+
+            (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.
+     */
+    private val readerActivity: ReaderActivity
+        get() = adapter.fragment.readerActivity
+
+    /**
+     * Property to get the webtoon reader.
+     */
+    private val webtoonReader: WebtoonReader
+        get() = adapter.fragment
+}

+ 0 - 204
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.java

@@ -1,204 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.webtoon;
-
-import android.os.Bundle;
-import android.support.v7.widget.RecyclerView;
-import android.view.GestureDetector;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.ArrayList;
-
-import eu.kanade.tachiyomi.data.database.models.Chapter;
-import eu.kanade.tachiyomi.data.source.model.Page;
-import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
-import eu.kanade.tachiyomi.widget.PreCachingLayoutManager;
-import rx.Subscription;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.subjects.PublishSubject;
-
-import static android.view.GestureDetector.SimpleOnGestureListener;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-
-public class WebtoonReader extends BaseReader {
-
-    private WebtoonAdapter adapter;
-    private RecyclerView recycler;
-    private PreCachingLayoutManager layoutManager;
-    private Subscription subscription;
-    private Subscription decoderSubscription;
-    protected GestureDetector gestureDetector;
-
-    private int scrollDistance;
-
-    private static final String SAVED_POSITION = "saved_position";
-
-    private static final float LEFT_REGION = 0.33f;
-    private static final float RIGHT_REGION = 0.66f;
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
-        adapter = new WebtoonAdapter(this);
-
-        int screenHeight = getResources().getDisplayMetrics().heightPixels;
-        scrollDistance = screenHeight * 3 / 4;
-
-        layoutManager = new PreCachingLayoutManager(getActivity());
-        layoutManager.setExtraLayoutSpace(screenHeight / 2);
-        if (savedState != null) {
-            layoutManager.scrollToPositionWithOffset(savedState.getInt(SAVED_POSITION), 0);
-        }
-
-        recycler = new RecyclerView(getActivity());
-        recycler.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
-        recycler.setLayoutManager(layoutManager);
-        recycler.setItemAnimator(null);
-        recycler.setAdapter(adapter);
-
-        decoderSubscription = getReaderActivity().getPreferences().imageDecoder()
-                .asObservable()
-                .doOnNext(this::setDecoderClass)
-                .skip(1)
-                .distinctUntilChanged()
-                .subscribe(v -> recycler.setAdapter(adapter));
-
-        gestureDetector = new GestureDetector(recycler.getContext(), new SimpleOnGestureListener() {
-            @Override
-            public boolean onSingleTapConfirmed(MotionEvent e) {
-                final float positionX = e.getX();
-
-                if (positionX < recycler.getWidth() * LEFT_REGION) {
-                    moveToPrevious();
-                } else if (positionX > recycler.getWidth() * RIGHT_REGION) {
-                    moveToNext();
-                } else {
-                    getReaderActivity().onCenterSingleTap();
-                }
-                return true;
-            }
-        });
-
-        setPages();
-        return recycler;
-    }
-
-    @Override
-    public void onDestroyView() {
-        decoderSubscription.unsubscribe();
-        super.onDestroyView();
-    }
-
-    @Override
-    public void onPause() {
-        unsubscribeStatus();
-        super.onPause();
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        int savedPosition = pages != null ?
-                pages.get(layoutManager.findFirstVisibleItemPosition()).getPageNumber() : 0;
-        outState.putInt(SAVED_POSITION, savedPosition);
-    }
-
-    private void unsubscribeStatus() {
-        if (subscription != null && !subscription.isUnsubscribed())
-            subscription.unsubscribe();
-    }
-
-    @Override
-    public void setSelectedPage(int pageNumber) {
-        recycler.scrollToPosition(pageNumber);
-    }
-
-    @Override
-    public void moveToNext() {
-        recycler.smoothScrollBy(0, scrollDistance);
-    }
-
-    @Override
-    public void moveToPrevious() {
-        recycler.smoothScrollBy(0, -scrollDistance);
-    }
-
-    @Override
-    public void onSetChapter(Chapter chapter, Page currentPage) {
-        pages = new ArrayList<>(chapter.getPages());
-        // Restoring current page is not supported. It's getting weird scrolling jumps
-        // this.currentPage = currentPage;
-
-        // This method can be called before the view is created
-        if (recycler != null) {
-            setPages();
-        }
-    }
-
-    @Override
-    public void onAppendChapter(Chapter chapter) {
-        int insertStart = pages.size();
-        pages.addAll(chapter.getPages());
-
-        // This method can be called before the view is created
-        if (recycler != null) {
-            adapter.setPages(pages);
-            adapter.notifyItemRangeInserted(insertStart, chapter.getPages().size());
-            if (subscription != null && subscription.isUnsubscribed()) {
-                observeStatus(insertStart);
-            }
-        }
-    }
-
-    private void setPages() {
-        if (pages != null) {
-            unsubscribeStatus();
-            recycler.clearOnScrollListeners();
-            adapter.setPages(pages);
-            recycler.setAdapter(adapter);
-            updatePageNumber();
-            setScrollListener();
-            observeStatus(0);
-        }
-    }
-
-    private void setScrollListener() {
-        recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
-            @Override
-            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
-                int page = layoutManager.findLastVisibleItemPosition();
-                if (page != currentPage) {
-                    onPageChanged(page);
-                }
-            }
-        });
-    }
-
-    private void observeStatus(int position) {
-        if (position == pages.size()) {
-            unsubscribeStatus();
-            return;
-        }
-
-        final Page page = pages.get(position);
-
-        PublishSubject<Integer> statusSubject = PublishSubject.create();
-        page.setStatusSubject(statusSubject);
-
-        // Unsubscribe from the previous page
-        unsubscribeStatus();
-
-        subscription = statusSubject
-                .startWith(page.getStatus())
-                .observeOn(AndroidSchedulers.mainThread())
-                .subscribe(status -> processStatus(position, status));
-    }
-
-    private void processStatus(int position, int status) {
-        adapter.notifyItemChanged(position);
-        if (status == Page.READY) {
-            observeStatus(position + 1);
-        }
-    }
-
-}

+ 203 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonReader.kt

@@ -0,0 +1,203 @@
+package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
+
+import android.os.Bundle
+import android.support.v7.widget.RecyclerView
+import android.view.*
+import android.view.GestureDetector.SimpleOnGestureListener
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
+import eu.kanade.tachiyomi.widget.PreCachingLayoutManager
+import rx.subscriptions.CompositeSubscription
+
+/**
+ * Implementation of a reader for webtoons based on a RecyclerView.
+ */
+class WebtoonReader : BaseReader() {
+
+    companion object {
+        /**
+         * Key to save and restore the position of the layout manager.
+         */
+        private val SAVED_POSITION = "saved_position"
+
+        /**
+         * Left side region of the screen. Used for touch events.
+         */
+        private val LEFT_REGION = 0.33f
+
+        /**
+         * Right side region of the screen. Used for touch events.
+         */
+        private val RIGHT_REGION = 0.66f
+    }
+
+    /**
+     * RecyclerView of the reader.
+     */
+    lateinit var recycler: RecyclerView
+        private set
+
+    /**
+     * Adapter of the recycler.
+     */
+    lateinit var adapter: WebtoonAdapter
+        private set
+
+    /**
+     * Layout manager of the recycler.
+     */
+    lateinit var layoutManager: PreCachingLayoutManager
+        private set
+
+    /**
+     * Gesture detector for touch events.
+     */
+    val gestureDetector by lazy { createGestureDetector() }
+
+    /**
+     * Subscriptions used while the view exists.
+     */
+    lateinit var subscriptions: CompositeSubscription
+        private set
+
+    private var scrollDistance: Int = 0
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
+        adapter = WebtoonAdapter(this)
+
+        val screenHeight = resources.displayMetrics.heightPixels
+        scrollDistance = screenHeight * 3 / 4
+
+        layoutManager = PreCachingLayoutManager(activity)
+        layoutManager.setExtraLayoutSpace(screenHeight / 2)
+        if (savedState != null) {
+            layoutManager.scrollToPositionWithOffset(savedState.getInt(SAVED_POSITION), 0)
+        }
+
+        recycler = RecyclerView(activity).apply {
+            layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+            itemAnimator = null
+        }
+        recycler.layoutManager = layoutManager
+        recycler.adapter = adapter
+        recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
+                val page = layoutManager.findLastVisibleItemPosition()
+                if (page != currentPage) {
+                    onPageChanged(page)
+                }
+            }
+        })
+
+        subscriptions = CompositeSubscription()
+        subscriptions.add(readerActivity.preferences.imageDecoder()
+                .asObservable()
+                .doOnNext { setDecoderClass(it) }
+                .skip(1)
+                .distinctUntilChanged()
+                .subscribe { recycler.adapter = adapter })
+
+        setPagesOnAdapter()
+        return recycler
+    }
+
+    override fun onDestroyView() {
+        subscriptions.unsubscribe()
+        super.onDestroyView()
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        val savedPosition = pages[layoutManager.findFirstVisibleItemPosition()].pageNumber
+        outState.putInt(SAVED_POSITION, savedPosition)
+        super.onSaveInstanceState(outState)
+    }
+
+    /**
+     * Creates the gesture detector for the reader.
+     *
+     * @return a gesture detector.
+     */
+    protected fun createGestureDetector(): GestureDetector {
+        return GestureDetector(context, object : SimpleOnGestureListener() {
+            override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+                val positionX = e.x
+
+                if (positionX < recycler.width * LEFT_REGION) {
+                    moveToPrevious()
+                } else if (positionX > recycler.width * RIGHT_REGION) {
+                    moveToNext()
+                } else {
+                    readerActivity.onCenterSingleTap()
+                }
+                return true
+            }
+        })
+    }
+
+    /**
+     * Called when a new chapter is set in [BaseReader].
+     *
+     * @param chapter the chapter set.
+     * @param currentPage the initial page to display.
+     */
+    override fun onChapterSet(chapter: Chapter, currentPage: Page) {
+        // Restoring current page is not supported. It's getting weird scrolling jumps
+        // this.currentPage = currentPage;
+
+        // Make sure the view is already initialized.
+        if (view != null) {
+            setPagesOnAdapter()
+        }
+    }
+
+    /**
+     * Called when a chapter is appended in [BaseReader].
+     *
+     * @param chapter the chapter appended.
+     */
+    override fun onChapterAppended(chapter: Chapter) {
+        // Make sure the view is already initialized.
+        if (view != null) {
+            val insertStart = pages.size - chapter.pages.size
+            adapter.notifyItemRangeInserted(insertStart, chapter.pages.size)
+        }
+    }
+
+    /**
+     * Sets the pages on the adapter.
+     */
+    private fun setPagesOnAdapter() {
+        if (pages.isNotEmpty()) {
+            adapter.pages = pages
+            recycler.adapter = adapter
+            updatePageNumber()
+        }
+    }
+
+    /**
+     * Sets the active page.
+     *
+     * @param pageNumber the index of the page from [pages].
+     */
+    override fun setActivePage(pageNumber: Int) {
+        recycler.scrollToPosition(pageNumber)
+    }
+
+    /**
+     * Moves to the next page or requests the next chapter if it's the last one.
+     */
+    override fun moveToNext() {
+        recycler.smoothScrollBy(0, scrollDistance)
+    }
+
+    /**
+     * Moves to the previous page or requests the previous chapter if it's the first one.
+     */
+    override fun moveToPrevious() {
+        recycler.smoothScrollBy(0, -scrollDistance)
+    }
+
+}

+ 1 - 1
app/src/main/res/layout/chapter_image.xml

@@ -3,4 +3,4 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:id="@+id/page_image_view" />
+    android:id="@+id/image_view" />

+ 1 - 1
app/src/main/res/layout/item_webtoon_reader.xml

@@ -2,7 +2,7 @@
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="wrap_content">
 
     <FrameLayout
         android:layout_width="match_parent"