Migrate to ViewPager2 (#1335)

* Migrate to ViewPager2

* Remove code for unimplemented automatic background
This commit is contained in:
FooIbar authored and GitHub committed 2024-06-21 12:07:27 +08:00
1 parent 33f83b7325
commit 1302e17c01
17 files changed
+187 -533

No files matched your search

+1 -1
View File
@@ -206,6 +206,7 @@ dependencies {
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.viewpager2)
// https://developer.android.com/jetpack/androidx/releases/room
ksp(libs.androidx.room.compiler)
@@ -213,7 +214,6 @@ dependencies {
implementation(libs.androidx.work.runtime)
implementation(libs.photoview)
implementation(libs.directionalviewpager) // Dead Dependency
implementation(libs.material.motion.core)
implementation(libs.bundles.splitties)
@@ -100,7 +100,6 @@ import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.isNightMode
@@ -354,7 +353,7 @@ class ReaderActivity : EhActivity() {
totalPage = mGalleryProvider!!.size
viewer?.destroy()
viewer = ReadingModeType.toViewer(ReaderPreferences.defaultReadingMode().get(), this)
isRtl = viewer is R2LPagerViewer
isRtl = viewer!!.isRtl
updateViewerInsets(ReaderPreferences.cutoutShort().get())
binding.viewerContainer.removeAllViews()
setOrientation(ReaderPreferences.defaultOrientationType().get())
@@ -12,9 +12,7 @@ import com.hippo.ehviewer.icons.reader.Vertical
import com.hippo.ehviewer.icons.reader.Webtoon
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.VerticalPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
@Stable
@@ -51,12 +49,12 @@ enum class ReadingModeType(
fun fromSpinner(position: Int?) = entries.find { value -> value.prefValue == position } ?: DEFAULT
fun toViewer(preference: Int?, activity: ReaderActivity): BaseViewer = when (fromPreference(preference)) {
LEFT_TO_RIGHT -> L2RPagerViewer(activity)
RIGHT_TO_LEFT -> R2LPagerViewer(activity)
VERTICAL -> VerticalPagerViewer(activity)
LEFT_TO_RIGHT -> PagerViewer(activity)
RIGHT_TO_LEFT -> PagerViewer(activity, true)
VERTICAL -> PagerViewer(activity, isVertical = true)
WEBTOON -> WebtoonViewer(activity)
CONTINUOUS_VERTICAL -> WebtoonViewer(activity, isContinuous = false)
DEFAULT -> L2RPagerViewer(activity)
DEFAULT -> PagerViewer(activity)
}
}
}
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@@ -10,6 +11,12 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
* Interface for implementing a viewer.
*/
interface BaseViewer {
val activity: ReaderActivity
val readerConfig: ReaderPageImageView.Config
val isRtl: Boolean
fun getViewForInsets() = getView()
/**
@@ -20,7 +27,7 @@ interface BaseViewer {
/**
* Destroys this viewer. Called when leaving the reader or swapping viewers.
*/
fun destroy() {}
fun destroy()
/**
* Tells this viewer to set the given [provider] as active.
@@ -1,16 +1,18 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
package eu.kanade.tachiyomi.ui.reader.viewer
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
/**
* RecyclerView Adapter used by this [viewer] to where [PageLoader] updates are posted.
*/
class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class ReaderPageAdapter(
val viewer: BaseViewer,
private val isWebtoon: Boolean = false,
) : RecyclerView.Adapter<ReaderPageHolder>() {
/**
* List of currently set items.
@@ -47,29 +49,25 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
/**
* Creates a new view holder for an item with the given [viewType].
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = ReaderPageImageView(readerThemedContext, isWebtoon = true)
return WebtoonPageHolder(view, viewer)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReaderPageHolder {
val view = ReaderPageImageView(readerThemedContext, isWebtoon = isWebtoon)
return ReaderPageHolder(view, viewer)
}
/**
* Binds an existing view [holder] with the item at the given [position].
*/
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
override fun onBindViewHolder(holder: ReaderPageHolder, position: Int) {
val item = items[position]
currentChapter!!.request(position)
when (holder) {
is WebtoonPageHolder -> holder.bind(item)
}
holder.bind(item)
}
/**
* Recycles an existing view [holder] before adding it to the view pool.
*/
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
override fun onViewRecycled(holder: ReaderPageHolder) {
currentChapter?.cancelRequest(holder.bindingAdapterPosition)
when (holder) {
is WebtoonPageHolder -> holder.recycle()
}
holder.recycle()
}
}
@@ -1,16 +1,15 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
package eu.kanade.tachiyomi.ui.reader.viewer
import android.content.Context
import android.content.res.Resources
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import androidx.recyclerview.widget.RecyclerView
import com.hippo.ehviewer.image.Image
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderErrorLayout
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import eu.kanade.tachiyomi.util.system.dpToPx
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -19,16 +18,21 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* Holder of the webtoon reader for a single page of a chapter.
* Holder of the reader for a single page of a chapter.
*
* @param frame the root view for this holder.
* @param viewer the webtoon viewer.
* @constructor creates a new webtoon holder.
* @param viewer the page viewer.
* @constructor creates a new page holder.
*/
class WebtoonPageHolder(
class ReaderPageHolder(
private val frame: ReaderPageImageView,
viewer: WebtoonViewer,
) : WebtoonBaseHolder(frame, viewer) {
private val viewer: BaseViewer,
) : RecyclerView.ViewHolder(frame) {
/**
* Context getter because it's used often.
*/
val context: Context get() = frame.context
/**
* Loading progress bar to indicate the current progress.
@@ -42,12 +46,6 @@ class WebtoonPageHolder(
*/
private var errorLayout: ReaderErrorLayout? = null
/**
* The default height when image not available.
*/
private val defaultHeight
get() = (viewer.recycler.width * 1.4125).toInt()
/**
* Page of a chapter.
*/
@@ -61,10 +59,16 @@ class WebtoonPageHolder(
private var statusJob: Job? = null
init {
frame.addView(progressIndicator, MATCH_PARENT, defaultHeight)
refreshLayoutParams()
if (viewer is WebtoonViewer) {
val defaultHeight = (viewer.recycler.width * 1.4125).toInt()
frame.addView(progressIndicator, MATCH_PARENT, defaultHeight)
refreshLayoutParams()
} else {
frame.addView(progressIndicator)
frame.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
frame.onImageLoaded = { onImageDecoded() }
frame.onImageLoaded = ::onImageLoaded
frame.onScaleChanged = { viewer.activity.hideMenu() }
}
@@ -83,21 +87,26 @@ class WebtoonPageHolder(
}
private fun refreshLayoutParams() {
frame.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
if (!viewer.isContinuous) {
bottomMargin = 15.dpToPx
}
if (viewer is WebtoonViewer) {
frame.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
if (!viewer.isContinuous) {
bottomMargin = 15.dpToPx
}
val margin = Resources.getSystem().displayMetrics.widthPixels * (viewer.config.sidePadding / 100f)
marginEnd = margin.toInt()
marginStart = margin.toInt()
val margin = Resources.getSystem().displayMetrics.widthPixels * (viewer.config.sidePadding / 100f)
marginEnd = margin.toInt()
marginStart = margin.toInt()
}
}
}
fun onPageSelected(forward: Boolean) = frame.onPageSelected(forward)
fun panEnd(reverse: Boolean) = if (reverse) frame.panLeft() else frame.panRight()
/**
* Called when the view is recycled and added to the view pool.
*/
override fun recycle() {
fun recycle() {
statusJob?.cancel()
statusJob = null
@@ -153,16 +162,9 @@ class WebtoonPageHolder(
/**
* Called when the page is ready.
*/
private fun setImage(drawable: Image) {
private fun setImage(image: Image) {
progressIndicator.setProgress(0)
frame.setImage(
drawable,
ReaderPageImageView.Config(
zoomDuration = viewer.config.doubleTapAnimDuration,
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH,
cropBorders = viewer.config.imageCropBorders,
),
)
frame.setImage(image, viewer.readerConfig)
}
/**
@@ -174,9 +176,9 @@ class WebtoonPageHolder(
}
/**
* Called when the image is decoded and going to be displayed.
* Called when the image is displayed.
*/
private fun onImageDecoded() {
private fun onImageLoaded() {
progressIndicator.hide()
removeErrorLayout()
}
@@ -14,7 +14,6 @@ import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import androidx.annotation.AttrRes
import androidx.annotation.CallSuper
import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.compose.ui.graphics.toAndroidRect
@@ -40,7 +39,7 @@ import io.getstream.photoview.PhotoView
* @param isWebtoon if true, [WebtoonSubsamplingImageView] will be used instead of [SubsamplingScaleImageView]
* and [AppCompatImageView] will be used instead of [PhotoView]
*/
open class ReaderPageImageView @JvmOverloads constructor(
class ReaderPageImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttrs: Int = 0,
@@ -60,18 +59,16 @@ open class ReaderPageImageView @JvmOverloads constructor(
*/
var pageBackground: Drawable? = null
@CallSuper
open fun onImageLoaded() {
private fun onImageLoaded() {
onImageLoaded?.invoke()
background = pageBackground
}
@CallSuper
open fun onScaleChanged(newScale: Float) {
fun onScaleChanged(newScale: Float) {
onScaleChanged?.invoke(newScale)
}
open fun onPageSelected(forward: Boolean) {
fun onPageSelected(forward: Boolean) {
with(pageView as? SubsamplingScaleImageView) {
if (this == null) return
if (isReady) {
@@ -125,16 +122,6 @@ open class ReaderPageImageView @JvmOverloads constructor(
it.isVisible = false
}
/**
* Check if the image can be panned to the left
*/
fun canPanLeft(): Boolean = canPan { it.left }
/**
* Check if the image can be panned to the right
*/
fun canPanRight(): Boolean = canPan { it.right }
/**
* Check whether the image can be panned.
* @param fn a function that returns the direction to check for
@@ -152,15 +139,23 @@ open class ReaderPageImageView @JvmOverloads constructor(
/**
* Pans the image to the left by a screen's width worth.
*/
fun panLeft() {
pan { center, view -> center.also { it.x -= view.width / view.scale } }
fun panLeft(): Boolean {
val canPan = canPan { it.left }
if (canPan) {
pan { center, view -> center.also { it.x -= view.width / view.scale } }
}
return canPan
}
/**
* Pans the image to the right by a screen's width worth.
*/
fun panRight() {
pan { center, view -> center.also { it.x += view.width / view.scale } }
fun panRight(): Boolean {
val canPan = canPan { it.right }
if (canPan) {
pan { center, view -> center.also { it.x += view.width / view.scale } }
}
return canPan
}
/**
@@ -96,10 +96,10 @@ class PagerConfig(
private fun zoomTypeFromPreference(value: Int) {
imageZoomType = when (value) {
// Auto
1 -> when (viewer) {
is L2RPagerViewer -> ReaderPageImageView.ZoomStartPosition.LEFT
is R2LPagerViewer -> ReaderPageImageView.ZoomStartPosition.RIGHT
else -> ReaderPageImageView.ZoomStartPosition.CENTER
1 -> when {
viewer.isRtl -> ReaderPageImageView.ZoomStartPosition.RIGHT
viewer.isVertical -> ReaderPageImageView.ZoomStartPosition.CENTER
else -> ReaderPageImageView.ZoomStartPosition.LEFT
}
// Left
2 -> ReaderPageImageView.ZoomStartPosition.LEFT
@@ -115,8 +115,8 @@ class PagerConfig(
field = value.also { it.invertMode = this.tappingInverted }
}
override fun defaultNavigation(): ViewerNavigation = when (viewer) {
is VerticalPagerViewer -> LNavigation()
override fun defaultNavigation(): ViewerNavigation = when {
viewer.isVertical -> LNavigation()
else -> RightAndLeftNavigation()
}
@@ -1,21 +1,17 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint
import android.content.Context
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.MotionEvent
import androidx.viewpager.widget.DirectionalViewPager
import android.widget.FrameLayout
import androidx.viewpager2.widget.ViewPager2
import eu.kanade.tachiyomi.ui.reader.viewer.GestureDetectorWithLongTap
/**
* Pager implementation that listens for tap and long tap and allows temporarily disabling touch
* events in order to work with child views that need to disable touch events on this parent. The
* pager can also be declared to be vertical by creating it with [isHorizontal] to false.
* Frame layout that wraps a [ViewPager2] to handle touch events because [ViewPager2] is final.
*/
open class Pager(
context: Context,
isHorizontal: Boolean = true,
) : DirectionalViewPager(context, isHorizontal) {
class PagerFrame(context: Context) : FrameLayout(context) {
/**
* Tap listener function to execute when a tap is detected.
@@ -79,6 +75,7 @@ open class Pager(
* Handles a touch event. Only used to prevent crashes when child views manipulate
* [requestDisallowInterceptTouchEvent].
*/
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent): Boolean = try {
super.onTouchEvent(ev)
} catch (e: NullPointerException) {
@@ -89,15 +86,6 @@ open class Pager(
false
}
/**
* Executes the given key event when this pager has focus. Just do nothing because the reader
* already dispatches key events to the viewer and has more control than this method.
*/
override fun executeKeyEvent(event: KeyEvent): Boolean {
// Disable viewpager's default key event handling
return false
}
/**
* Enables or disables the gesture detector.
*/
@@ -1,182 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint
import android.content.Context
import coil3.BitmapImage
import com.hippo.ehviewer.image.Image
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderErrorLayout
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* View of the ViewPager that contains a page of a chapter.
*/
@SuppressLint("ViewConstructor")
class PagerPageHolder(
readerThemedContext: Context,
val viewer: PagerViewer,
val page: ReaderPage,
) : ReaderPageImageView(readerThemedContext),
ViewPagerAdapter.PositionableView {
/**
* Item that identifies this view. Needed by the adapter to not recreate views.
*/
override val item
get() = page
/**
* Loading progress bar to indicate the current progress.
*/
private val progressIndicator = ReaderProgressIndicator(readerThemedContext)
/**
* Error layout to show when the image fails to load.
*/
private var errorLayout: ReaderErrorLayout? = null
private val scope = CoroutineScope(Dispatchers.IO)
/**
* Subscription for status changes of the page.
*/
private var statusJob: Job? = null
init {
addView(progressIndicator)
statusJob = scope.launch(Dispatchers.Main) {
page.status.collectLatest {
processStatus(it)
}
}
}
/**
* Called when this view is detached from the window. Unsubscribes any active subscription.
*/
@SuppressLint("ClickableViewAccessibility")
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
unsubscribeStatus()
progressIndicator.hide()
}
/**
* Called when the status of the page changes.
*
* @param status the new status of the page.
*/
private suspend fun processStatus(status: Page.State) {
when (status) {
Page.State.QUEUE -> setQueued()
Page.State.LOAD_PAGE -> setLoading()
Page.State.DOWNLOAD_IMAGE -> {
setDownloading()
page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
}
Page.State.READY -> page.image?.let { setImage(it) } ?: setError()
Page.State.ERROR -> setError()
}
}
/**
* Unsubscribes from the status subscription.
*/
private fun unsubscribeStatus() {
statusJob?.cancel()
statusJob = null
}
/**
* Called when the page is queued.
*/
private fun setQueued() {
progressIndicator.setProgress(0)
progressIndicator.show()
removeErrorLayout()
recycle()
}
/**
* Called when the page is loading.
*/
private fun setLoading() {
progressIndicator.show()
removeErrorLayout()
}
/**
* Called when the page is downloading.
*/
private fun setDownloading() {
progressIndicator.show()
removeErrorLayout()
}
/**
* Called when the page is ready.
*/
private fun setImage(image: Image) {
progressIndicator.setProgress(0)
setImage(
image,
Config(
zoomDuration = viewer.config.doubleTapAnimDuration,
minimumScaleType = viewer.config.imageScaleType,
cropBorders = viewer.config.imageCropBorders,
zoomStartPosition = viewer.config.imageZoomType,
landscapeZoom = viewer.config.landscapeZoom,
),
)
if (image.innerImage is BitmapImage) {
pageBackground = background
}
}
/**
* Called when the page has an error.
*/
private fun setError() {
progressIndicator.hide()
showErrorLayout()
}
override fun onImageLoaded() {
super.onImageLoaded()
progressIndicator.hide()
removeErrorLayout()
}
/**
* Called when an image is zoomed in/out.
*/
override fun onScaleChanged(newScale: Float) {
super.onScaleChanged(newScale)
viewer.activity.hideMenu()
}
private fun showErrorLayout() {
if (errorLayout == null) {
errorLayout = ReaderErrorLayout(context, page.errorMsg) {
viewer.activity.retryPage(page.index)
}.also {
addView(it)
}
}
}
private fun removeErrorLayout() {
errorLayout?.let {
removeView(it)
errorLayout = null
}
}
}
@@ -6,43 +6,57 @@ import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup.LayoutParams
import androidx.core.view.children
import androidx.core.view.get
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.viewpager.widget.ViewPager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.hippo.ehviewer.R
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageAdapter
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageHolder
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
/**
* Implementation of a [BaseViewer] to display pages with a [ViewPager].
* Implementation of a [BaseViewer] to display pages with a [ViewPager2].
*/
@Suppress("LeakingThis")
abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
class PagerViewer(
override val activity: ReaderActivity,
override val isRtl: Boolean = false,
val isVertical: Boolean = false,
) : BaseViewer {
private val scope = MainScope()
/**
* View pager used by this viewer. It's abstract to implement L2R, R2L and vertical pagers on
* top of this class.
* View pager used by this viewer.
*/
val pager = createPager()
private val pager = ViewPager2(activity).apply {
if (isRtl) {
layoutDirection = ViewPager2.LAYOUT_DIRECTION_RTL
} else if (isVertical) {
orientation = ViewPager2.ORIENTATION_VERTICAL
}
}
private val frame = PagerFrame(activity)
/**
* Configuration used by the pager, like allow taps, scale mode on images, page transitions...
*/
val config = PagerConfig(this, scope)
private val config = PagerConfig(this, scope)
/**
* Adapter of the pager.
*/
private val adapter = PagerViewerAdapter(this)
private val adapter = ReaderPageAdapter(this)
/**
* Currently active item. It can be a chapter page or a chapter transition.
@@ -80,8 +94,8 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
pager.offscreenPageLimit = 1
pager.id = R.id.reader_pager
pager.adapter = adapter
pager.addOnPageChangeListener(
object : ViewPager.SimpleOnPageChangeListener() {
pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
if (activity.isScrollingThroughPages.not()) {
activity.hideMenu()
@@ -91,11 +105,11 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
}
override fun onPageScrollStateChanged(state: Int) {
isIdle = state == ViewPager.SCROLL_STATE_IDLE
isIdle = state == ViewPager2.SCROLL_STATE_IDLE
}
},
)
pager.tapListener = { event ->
frame.tapListener = { event ->
val windowOffset = IntArray(2)
activity.window.decorView.getLocationOnScreen(windowOffset)
val pos = PointF(
@@ -112,7 +126,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
NavigationRegion.LEFT -> moveLeft()
}
}
pager.longTapListener = f@{
frame.longTapListener = f@{
if (activity.menuVisible || config.longTapEnabled) {
val item = adapter.items.getOrNull(pager.currentItem)
if (item is ReaderPage) {
@@ -131,30 +145,34 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
val showOnStart = config.navigationOverlayOnStart || config.forceNavigationOverlay
activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
}
frame.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
frame.addView(pager)
}
override val readerConfig
get() = ReaderPageImageView.Config(
zoomDuration = config.doubleTapAnimDuration,
minimumScaleType = config.imageScaleType,
cropBorders = config.imageCropBorders,
zoomStartPosition = config.imageZoomType,
landscapeZoom = config.landscapeZoom,
)
override fun destroy() {
super.destroy()
scope.cancel()
}
/**
* Creates a new ViewPager.
*/
abstract fun createPager(): Pager
/**
* Returns the view this viewer uses.
*/
override fun getView(): View = pager
override fun getView(): View = frame
/**
* Returns the PagerPageHolder for the provided page
* Returns the [ReaderPageHolder] for the provided page
*/
private fun getPageHolder(page: ReaderPage): PagerPageHolder? =
pager.children
.filterIsInstance(PagerPageHolder::class.java)
.firstOrNull { it.item == page }
private fun getPageHolder(position: Int): ReaderPageHolder? =
(pager[0] as RecyclerView).findViewHolderForAdapterPosition(position) as ReaderPageHolder?
/**
* Called when a new [ReaderPage] is marked as active
@@ -176,7 +194,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
activity.onPageSelected(page)
// Notify holder of page change
getPageHolder(page)?.onPageSelected(forward)
getPageHolder(page.index)?.onPageSelected(forward)
}
/**
@@ -227,56 +245,62 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
/**
* Moves to the next page.
*/
open fun moveToNext() {
moveRight()
private fun moveToNext() {
val currentItem = pager.currentItem
if (currentItem != adapter.itemCount - 1) {
val holder = getPageHolder(currentItem)
if (!config.navigateToPan || holder?.panEnd(isRtl) != true) {
pager.setCurrentItem(currentItem + 1, config.usePageTransitions)
}
}
}
/**
* Moves to the previous page.
*/
open fun moveToPrevious() {
moveLeft()
private fun moveToPrevious() {
val currentItem = pager.currentItem
if (currentItem != 0) {
val holder = getPageHolder(currentItem)
if (!config.navigateToPan || holder?.panEnd(!isRtl) != true) {
pager.setCurrentItem(currentItem - 1, config.usePageTransitions)
}
}
}
/**
* Moves to the page at the right.
*/
protected open fun moveRight() {
if (pager.currentItem != adapter.count - 1) {
val holder = currentPage?.let { getPageHolder(it) }
if (holder != null && config.navigateToPan && holder.canPanRight()) {
holder.panRight()
} else {
pager.setCurrentItem(pager.currentItem + 1, config.usePageTransitions)
}
private fun moveRight() {
if (isRtl) {
moveToPrevious()
} else {
moveToNext()
}
}
/**
* Moves to the page at the left.
*/
protected open fun moveLeft() {
if (pager.currentItem != 0) {
val holder = currentPage?.let { getPageHolder(it) }
if (holder != null && config.navigateToPan && holder.canPanLeft()) {
holder.panLeft()
} else {
pager.setCurrentItem(pager.currentItem - 1, config.usePageTransitions)
}
private fun moveLeft() {
if (isRtl) {
moveToNext()
} else {
moveToPrevious()
}
}
/**
* Moves to the page at the top (or previous).
*/
protected open fun moveUp() {
private fun moveUp() {
moveToPrevious()
}
/**
* Moves to the page at the bottom (or next).
*/
protected open fun moveDown() {
private fun moveDown() {
moveToNext()
}
@@ -1,81 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.view.View
import android.view.ViewGroup
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
/**
* Pager adapter used by this [viewer] to where [PageLoader] updates are posted.
*/
class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
/**
* List of currently set items.
*/
var items: List<ReaderPage> = emptyList()
private set
private var currentChapter: PageLoader? = null
/**
* Context that has been wrapped to use the correct theme values based on the
* current app theme and reader background color
*/
private var readerThemedContext = viewer.activity.createReaderThemeContext()
/**
* Updates this adapter with the given [chapters]. It handles setting a few pages of the
* next/previous chapter to allow seamless transitions and inverting the pages if the viewer
* has R2L direction.
*/
fun setChapters(chapters: PageLoader) {
items = chapters.pages
currentChapter = chapters
if (viewer is R2LPagerViewer) {
items = items.asReversed()
}
}
/**
* Returns the amount of items of the adapter.
*/
override fun getCount(): Int = items.size
/**
* Creates a new view for the item at the given [position].
*/
override fun createView(container: ViewGroup, position: Int): View {
val item = items[position]
currentChapter?.request(item.index)
return PagerPageHolder(readerThemedContext, viewer, item)
}
override fun destroyView(container: ViewGroup, position: Int, view: View) {
val item = (view as PagerPageHolder).item
currentChapter?.cancelRequest(item.index)
}
/**
* Returns the current position of the given [view] on the adapter.
*/
override fun getItemPosition(view: Any): Int {
if (view is PositionableView) {
val position = items.indexOf(view.item)
if (position != -1) {
return position
} else {
logcat { "Position for ${view.item} not found" }
}
}
return POSITION_NONE
}
fun refresh() {
readerThemedContext = viewer.activity.createReaderThemeContext()
}
}
@@ -1,47 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
/**
* Implementation of a left to right PagerViewer.
*/
class L2RPagerViewer(activity: ReaderActivity) : PagerViewer(activity) {
/**
* Creates a new left to right pager.
*/
override fun createPager(): Pager = Pager(activity)
}
/**
* Implementation of a right to left PagerViewer.
*/
class R2LPagerViewer(activity: ReaderActivity) : PagerViewer(activity) {
/**
* Creates a new right to left pager.
*/
override fun createPager(): Pager = Pager(activity)
/**
* Moves to the next page. On a R2L pager the next page is the one at the left.
*/
override fun moveToNext() {
moveLeft()
}
/**
* Moves to the previous page. On a R2L pager the previous page is the one at the right.
*/
override fun moveToPrevious() {
moveRight()
}
}
/**
* Implementation of a vertical (top to bottom) PagerViewer.
*/
class VerticalPagerViewer(activity: ReaderActivity) : PagerViewer(activity) {
/**
* Creates a new vertical pager.
*/
override fun createPager(): Pager = Pager(activity, isHorizontal = false)
}
@@ -1,29 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.content.Context
import android.view.View
import android.view.ViewGroup.LayoutParams
import androidx.recyclerview.widget.RecyclerView
abstract class WebtoonBaseHolder(
view: View,
protected val viewer: WebtoonViewer,
) : RecyclerView.ViewHolder(view) {
/**
* Context getter because it's used often.
*/
val context: Context get() = itemView.context
/**
* Called when the view is recycled and being added to the view pool.
*/
open fun recycle() {}
/**
* Extension method to set layout params to wrap content on this view.
*/
protected fun View.wrapContent() {
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
}
}
@@ -11,10 +11,13 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.WebtoonLayoutManager
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageAdapter
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.MainScope
@@ -23,7 +26,9 @@ import kotlinx.coroutines.cancel
/**
* Implementation of a [BaseViewer] to display pages with a [RecyclerView].
*/
class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true) : BaseViewer {
class WebtoonViewer(override val activity: ReaderActivity, val isContinuous: Boolean = true) : BaseViewer {
override val isRtl: Boolean = false
private val scope = MainScope()
@@ -50,7 +55,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
/**
* Adapter of the recycler view.
*/
private val adapter = WebtoonAdapter(this)
private val adapter = ReaderPageAdapter(this, true)
/**
* Distance to scroll when the user taps on one side of the recycler view.
@@ -132,6 +137,13 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
frame.addView(recycler)
}
override val readerConfig
get() = ReaderPageImageView.Config(
zoomDuration = config.doubleTapAnimDuration,
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH,
cropBorders = config.imageCropBorders,
)
override fun getViewForInsets(): View = recycler
/**
@@ -143,7 +155,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
* Destroys this viewer. Called when leaving the reader or swapping viewers.
*/
override fun destroy() {
super.destroy()
scope.cancel()
}
@@ -1,31 +0,0 @@
package eu.kanade.tachiyomi.widget
import android.view.View
import android.view.ViewGroup
import androidx.viewpager.widget.PagerAdapter
abstract class ViewPagerAdapter : PagerAdapter() {
protected abstract fun createView(container: ViewGroup, position: Int): View
protected open fun destroyView(container: ViewGroup, position: Int, view: View) {
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = createView(container, position)
container.addView(view)
return view
}
override fun destroyItem(container: ViewGroup, position: Int, obj: Any) {
val view = obj as View
destroyView(container, position, view)
container.removeView(view)
}
override fun isViewFromObject(view: View, obj: Any): Boolean = view === obj
interface PositionableView {
val item: Any
}
}
+3 -1
View File
@@ -15,6 +15,7 @@ androidx-lifecycle = "2.8.2"
androidx-paging = "3.3.0"
androidx-recyclerview = "1.4.0-alpha01"
androidx-room = "2.7.0-alpha04"
androidx-viewpager2 = "1.1.0"
androidx-work = "2.10.0-alpha02"
arrow = "1.2.4"
coil = "3.0.0-alpha06"
@@ -55,6 +56,8 @@ androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" }
androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "androidx-room" }
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "androidx-viewpager2" }
androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "androidx-work" }
arrow-fx-coroutines = { module = "io.arrow-kt:arrow-fx-coroutines" }
@@ -84,7 +87,6 @@ cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31"
desugar = "com.android.tools:desugar_jdk_libs_nio:2.0.4"
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
diff = "io.github.petertrr:kotlin-multiplatform-diff:0.6.0"
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
jsoup = "org.jsoup:jsoup:1.17.2"