Readers in Kotlin. Also fix #193

This commit is contained in:
len 2016-03-04 14:10:41 +01:00
parent b2fe9d7d4d
commit ff61282104
32 changed files with 1730 additions and 1394 deletions

View file

@ -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);
}

View file

@ -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;

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -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);
}
}

View file

@ -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)
}
}
}

View file

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

View file

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

View file

@ -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;
}
}
}

View file

@ -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
}
}
}

View file

@ -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);
}
}

View file

@ -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)
}
}

View file

@ -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();
}
}

View file

@ -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
}

View file

@ -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);
}
});
}
}

View file

@ -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)
}
})
}
}

View file

@ -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;
}
}

View file

@ -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) }
}
}

View file

@ -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();
}
}

View file

@ -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()
}
}

View file

@ -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);
}
});
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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) }
}
}

View file

@ -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();
}
}

View file

@ -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()
}
}

View file

@ -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);
}
}

View file

@ -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
}

View file

@ -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);
}
}
}

View file

@ -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)
}
}

View file

@ -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" />

View file

@ -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"