diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b081776aa..150d1850d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 83796936e..428ef3474 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -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()) diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt index 2c9247a26..a86451156 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt @@ -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) } } } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt index 56ceae857..d43ff3a6c 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt @@ -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. diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageAdapter.kt similarity index 74% rename from app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt rename to app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageAdapter.kt index fd771913a..04bb1e6e6 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonAdapter.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageAdapter.kt @@ -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() { +class ReaderPageAdapter( + val viewer: BaseViewer, + private val isWebtoon: Boolean = false, +) : RecyclerView.Adapter() { /** * List of currently set items. @@ -47,29 +49,25 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter 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() } } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageHolder.kt similarity index 70% rename from app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt rename to app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageHolder.kt index e1884da5f..5a78774dc 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageHolder.kt @@ -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() } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index 7e60db9cd..ac3003082 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -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 } /** diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt index 00e8bab0c..2cf93a60b 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt @@ -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() } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerFrame.kt similarity index 75% rename from app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt rename to app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerFrame.kt index fa478b673..e9bc6eb7c 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/Pager.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerFrame.kt @@ -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. */ diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt deleted file mode 100644 index cf46c622d..000000000 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index f4d218f57..c6b3105a6 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -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() } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt deleted file mode 100644 index e0540f3eb..000000000 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt +++ /dev/null @@ -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 = 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() - } -} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt deleted file mode 100644 index 1e9b78ede..000000000 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewers.kt +++ /dev/null @@ -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) -} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt deleted file mode 100644 index cbded3e7a..000000000 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonBaseHolder.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index 7a0bcb6d7..bce0167f5 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -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() } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt deleted file mode 100644 index ee7f3b036..000000000 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/widget/ViewPagerAdapter.kt +++ /dev/null @@ -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 - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 32c49a193..6835871b2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"