From f881cc439a2ac5ca743b3fa50c15a3bfc0066f71 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 10 Oct 2023 09:16:16 +0300 Subject: [PATCH] Draft double reader implementation --- .../reader/domain/DetectReaderModeUseCase.kt | 2 +- .../kotatsu/reader/ui/ReaderActivity.kt | 2 +- .../kotatsu/reader/ui/ReaderManager.kt | 21 +- .../ui/pager/doublepage/DoublePageHolder.kt | 48 +++ .../doublepage/DoublePageLayoutManager.kt | 19 ++ .../doublepage/DoublePageReaderFragment.kt | 128 ++++++++ .../pager/doublepage/DoublePageSnapHelper.kt | 280 ++++++++++++++++++ .../ui/pager/doublepage/DoublePagesAdapter.kt | 35 +++ .../res/layout/fragment_reader_double.xml | 10 + 9 files changed, 536 insertions(+), 9 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageHolder.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageLayoutManager.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageReaderFragment.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageSnapHelper.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePagesAdapter.kt create mode 100644 app/src/main/res/layout/fragment_reader_double.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt index 049914d3c..f5eae4021 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt @@ -13,11 +13,11 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.reader.ui.ReaderState -import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.InputStream import java.util.zip.ZipFile import javax.inject.Inject diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index a1c47e6f0..c80aacf81 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -105,7 +105,7 @@ class ReaderActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityReaderBinding.inflate(layoutInflater)) - readerManager = ReaderManager(supportFragmentManager, R.id.container) + readerManager = ReaderManager(supportFragmentManager, viewBinding.container) supportActionBar?.setDisplayHomeAsUpEnabled(true) touchHelper = GridTouchHelper(this, this) scrollTimer = scrollTimerFactory.create(this, this) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt index 4808b347c..5ac73ef59 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt @@ -1,10 +1,12 @@ package org.koitharu.kotatsu.reader.ui -import androidx.annotation.IdRes +import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment +import org.koitharu.kotatsu.reader.ui.pager.doublepage.DoublePageReaderFragment import org.koitharu.kotatsu.reader.ui.pager.reversed.ReversedReaderFragment import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment import org.koitharu.kotatsu.reader.ui.pager.vertical.VerticalReaderFragment @@ -13,20 +15,25 @@ import java.util.EnumMap class ReaderManager( private val fragmentManager: FragmentManager, - @IdRes private val containerResId: Int, + private val container: FragmentContainerView, ) { private val modeMap = EnumMap>>(ReaderMode::class.java) init { - modeMap[ReaderMode.STANDARD] = PagerReaderFragment::class.java + val isTablet = container.resources.getBoolean(R.bool.is_tablet) + modeMap[ReaderMode.STANDARD] = if (isTablet) { + DoublePageReaderFragment::class.java + } else { + PagerReaderFragment::class.java + } modeMap[ReaderMode.REVERSED] = ReversedReaderFragment::class.java modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java modeMap[ReaderMode.VERTICAL] = VerticalReaderFragment::class.java } val currentReader: BaseReaderFragment<*>? - get() = fragmentManager.findFragmentById(containerResId) as? BaseReaderFragment<*> + get() = fragmentManager.findFragmentById(container.id) as? BaseReaderFragment<*> val currentMode: ReaderMode? get() { @@ -38,14 +45,14 @@ class ReaderManager( val readerClass = requireNotNull(modeMap[newMode]) fragmentManager.commit { setReorderingAllowed(true) - replace(containerResId, readerClass, null, null) + replace(container.id, readerClass, null, null) } } - fun replace(reader: BaseReaderFragment<*>) { + /*fun replace(reader: BaseReaderFragment<*>) { fragmentManager.commit { setReorderingAllowed(true) replace(containerResId, reader) } - } + }*/ } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageHolder.kt new file mode 100644 index 000000000..a537d3f08 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageHolder.kt @@ -0,0 +1,48 @@ +package org.koitharu.kotatsu.reader.ui.pager.doublepage + +import android.graphics.PointF +import android.view.Gravity +import android.widget.FrameLayout +import androidx.lifecycle.LifecycleOwner +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver +import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.databinding.ItemPageBinding +import org.koitharu.kotatsu.reader.domain.PageLoader +import org.koitharu.kotatsu.reader.ui.config.ReaderSettings +import org.koitharu.kotatsu.reader.ui.pager.ReaderPage +import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder + +class DoublePageHolder( + owner: LifecycleOwner, + binding: ItemPageBinding, + loader: PageLoader, + settings: ReaderSettings, + networkState: NetworkState, + exceptionResolver: ExceptionResolver, +) : PageHolder(owner, binding, loader, settings, networkState, exceptionResolver) { + + private val isEven: Boolean + get() = bindingAdapterPosition and 1 == 0 + + override fun onBind(data: ReaderPage) { + super.onBind(data) + (binding.textViewNumber.layoutParams as FrameLayout.LayoutParams) + .gravity = (if (isEven) Gravity.START else Gravity.END) or Gravity.BOTTOM + } + + override fun onImageShowing(settings: ReaderSettings) { + with(binding.ssiv) { + maxScale = 2f * maxOf( + width / sWidth.toFloat(), + height / sHeight.toFloat(), + ) + binding.ssiv.colorFilter = settings.colorFilter?.toColorFilter() + minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE + setScaleAndCenter( + minScale, + PointF(if (isEven) sWidth.toFloat() else 0f, 0f), + ) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageLayoutManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageLayoutManager.kt new file mode 100644 index 000000000..a2930fa53 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageLayoutManager.kt @@ -0,0 +1,19 @@ +package org.koitharu.kotatsu.reader.ui.pager.doublepage + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class DoublePageLayoutManager( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, +) : LinearLayoutManager(context, attrs, defStyleAttr, defStyleRes) { + + override fun checkLayoutParams(lp: RecyclerView.LayoutParams?): Boolean { + lp?.width = width / 2 + return super.checkLayoutParams(lp) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageReaderFragment.kt new file mode 100644 index 000000000..6afb4d958 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageReaderFragment.kt @@ -0,0 +1,128 @@ +package org.koitharu.kotatsu.reader.ui.pager.doublepage + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.yield +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition +import org.koitharu.kotatsu.databinding.FragmentReaderDoubleBinding +import org.koitharu.kotatsu.parsers.util.toIntUp +import org.koitharu.kotatsu.reader.domain.PageLoader +import org.koitharu.kotatsu.reader.ui.ReaderState +import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter +import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment +import org.koitharu.kotatsu.reader.ui.pager.ReaderPage +import javax.inject.Inject +import kotlin.math.absoluteValue + +@AndroidEntryPoint +class DoublePageReaderFragment : BaseReaderFragment() { + + @Inject + lateinit var networkState: NetworkState + + @Inject + lateinit var pageLoader: PageLoader + + override fun onCreateViewBinding( + inflater: LayoutInflater, + container: ViewGroup?, + ) = FragmentReaderDoubleBinding.inflate(inflater, container, false) + + override fun onViewBindingCreated( + binding: FragmentReaderDoubleBinding, + savedInstanceState: Bundle?, + ) { + super.onViewBindingCreated(binding, savedInstanceState) + with(binding.recyclerView) { + adapter = readerAdapter + addOnScrollListener(PageScrollListener()) + DoublePageSnapHelper().attachToRecyclerView(this) + } + } + + override fun onDestroyView() { + requireViewBinding().recyclerView.adapter = null + super.onDestroyView() + } + + override suspend fun onPagesChanged(pages: List, pendingState: ReaderState?) = + coroutineScope { + val items = async { + requireAdapter().setItems(pages) + yield() + } + if (pendingState != null) { + val position = pages.indexOfFirst { + it.chapterId == pendingState.chapterId && it.index == pendingState.page + } + items.await() + if (position != -1) { + requireViewBinding().recyclerView.firstVisibleItemPosition = position or 1 + notifyPageChanged(position) + } else { + Snackbar.make(requireView(), R.string.not_found_404, Snackbar.LENGTH_SHORT) + .show() + } + } else { + items.await() + } + } + + override fun onCreateAdapter() = DoublePagesAdapter( + lifecycleOwner = viewLifecycleOwner, + loader = pageLoader, + settings = viewModel.readerSettings, + networkState = networkState, + exceptionResolver = exceptionResolver, + ) + + override fun switchPageBy(delta: Int) { + switchPageTo((requireViewBinding().recyclerView.currentItem() + delta) or 1, delta.absoluteValue > 1) + } + + override fun switchPageTo(position: Int, smooth: Boolean) { + requireViewBinding().recyclerView.firstVisibleItemPosition = position or 1 + } + + override fun getCurrentState(): ReaderState? = viewBinding?.run { + val adapter = recyclerView.adapter as? BaseReaderAdapter<*> + val page = adapter?.getItemOrNull(recyclerView.currentItem()) ?: return@run null + ReaderState( + chapterId = page.chapterId, + page = page.index, + scroll = 0, + ) + } + + private fun notifyPageChanged(page: Int) { + viewModel.onCurrentPageChanged(page) + } + + private fun RecyclerView.currentItem(): Int { + val lm = layoutManager as LinearLayoutManager + return ((lm.findFirstVisibleItemPosition() + lm.findLastVisibleItemPosition()) / 2f).toIntUp() + } + + private inner class PageScrollListener : RecyclerView.OnScrollListener() { + + private var lastPage = RecyclerView.NO_POSITION + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + val page = recyclerView.currentItem() + if (page != lastPage) { + lastPage = page + notifyPageChanged(page) + } + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageSnapHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageSnapHelper.kt new file mode 100644 index 000000000..479f5b63e --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePageSnapHelper.kt @@ -0,0 +1,280 @@ +package org.koitharu.kotatsu.reader.ui.pager.doublepage + +import android.util.DisplayMetrics +import android.view.View +import android.view.animation.Interpolator +import android.widget.Scroller +import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.OrientationHelper +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider +import androidx.recyclerview.widget.SnapHelper +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt + +class DoublePageSnapHelper : SnapHelper() { + + private lateinit var recyclerView: RecyclerView + + // Total number of items in a block of view in the RecyclerView + private var blockSize = 2 + + // Maximum number of positions to move on a fling. + private var maxPositionsToMove = 0 + + // Width of a RecyclerView item if orientation is horizontal; height of the item if vertical + private var itemDimension = 0 + + // Maxim blocks to move during most vigorous fling. + private val maxFlingBlocks = 2 + + // When snapping, used to determine direction of snap. + private var priorFirstPosition = RecyclerView.NO_POSITION + + // Our private scroller + private var scroller: Scroller? = null + + // Horizontal/vertical layout helper + private lateinit var orientationHelper: OrientationHelper + + // LTR/RTL helper + private lateinit var layoutDirectionHelper: LayoutDirectionHelper + + private val snapInterpolator = Interpolator { input -> + var t = input + t -= 1.0f + t * t * t + 1.0f + } + + @Throws(IllegalStateException::class) + override fun attachToRecyclerView(target: RecyclerView?) { + if (target != null) { + recyclerView = target + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + check(layoutManager.canScrollHorizontally()) { "RecyclerView must be scrollable" } + orientationHelper = OrientationHelper.createHorizontalHelper(layoutManager) + layoutDirectionHelper = LayoutDirectionHelper(ViewCompat.getLayoutDirection(recyclerView)) + scroller = Scroller(target.context, snapInterpolator) + initItemDimensionIfNeeded(layoutManager) + } + super.attachToRecyclerView(recyclerView) + } + + override fun calculateDistanceToFinalSnap( + layoutManager: RecyclerView.LayoutManager, + targetView: View + ): IntArray { + val out = IntArray(2) + if (layoutManager.canScrollHorizontally()) { + out[0] = layoutDirectionHelper.getScrollToAlignView(targetView) + } + if (layoutManager.canScrollVertically()) { + out[1] = layoutDirectionHelper.getScrollToAlignView(targetView) + } + return out + } + + // We are flinging and need to know where we are heading. + override fun findTargetSnapPosition( + layoutManager: RecyclerView.LayoutManager, + velocityX: Int, velocityY: Int + ): Int { + val lm = layoutManager as LinearLayoutManager + initItemDimensionIfNeeded(layoutManager) + scroller!!.fling(0, 0, velocityX, velocityY, Int.MIN_VALUE, Int.MAX_VALUE, Int.MIN_VALUE, Int.MAX_VALUE) + if (velocityX != 0) { + return layoutDirectionHelper + .getPositionsToMove(lm, scroller!!.finalX, itemDimension) + } + return if (velocityY != 0) { + layoutDirectionHelper + .getPositionsToMove(lm, scroller!!.finalY, itemDimension) + } else RecyclerView.NO_POSITION + } + + // We have scrolled to the neighborhood where we will snap. Determine the snap position. + override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? { + // Snap to a view that is either 1) toward the bottom of the data and therefore on screen, + // or, 2) toward the top of the data and may be off-screen. + val snapPos: Int = calcTargetPosition(layoutManager as LinearLayoutManager) + return if (snapPos == RecyclerView.NO_POSITION) null else layoutManager.findViewByPosition(snapPos) + } + + // Does the heavy lifting for findSnapView. + private fun calcTargetPosition(layoutManager: LinearLayoutManager): Int { + val snapPos: Int + val firstVisiblePos = layoutManager.findFirstVisibleItemPosition() + if (firstVisiblePos == RecyclerView.NO_POSITION) { + return RecyclerView.NO_POSITION + } + initItemDimensionIfNeeded(layoutManager) + if (firstVisiblePos >= priorFirstPosition) { + // Scrolling toward bottom of data + val firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition() + snapPos = if (firstCompletePosition != RecyclerView.NO_POSITION + && firstCompletePosition % blockSize == 0 + ) { + firstCompletePosition + } else { + roundDownToBlockSize(firstVisiblePos + blockSize) + } + } else { + // Scrolling toward top of data + snapPos = roundDownToBlockSize(firstVisiblePos) + // Check to see if target view exists. If it doesn't, force a smooth scroll. + // SnapHelper only snaps to existing views and will not scroll to a non-existent one. + // If limiting fling to single block, then the following is not needed since the + // views are likely to be in the RecyclerView pool. + if (layoutManager.findViewByPosition(snapPos) == null) { + val toScroll: IntArray = layoutDirectionHelper.calculateDistanceToScroll(layoutManager, snapPos) + recyclerView.smoothScrollBy(toScroll[0], toScroll[1], snapInterpolator) + } + } + priorFirstPosition = firstVisiblePos + return snapPos + } + + private fun initItemDimensionIfNeeded(layoutManager: RecyclerView.LayoutManager) { + if (itemDimension != 0) { + return + } + val child: View = layoutManager.getChildAt(0) ?: return + if (layoutManager.canScrollHorizontally()) { + itemDimension = child.width + blockSize = getSpanCount(layoutManager) * (recyclerView.width / itemDimension) + } else if (layoutManager.canScrollVertically()) { + itemDimension = child.height + blockSize = getSpanCount(layoutManager) * (recyclerView.height / itemDimension) + } + maxPositionsToMove = blockSize * maxFlingBlocks + } + + private fun getSpanCount(layoutManager: RecyclerView.LayoutManager): Int { + return if (layoutManager is GridLayoutManager) layoutManager.spanCount else 1 + } + + private fun roundDownToBlockSize(trialPosition: Int): Int { + return trialPosition - trialPosition % blockSize + } + + private fun roundUpToBlockSize(trialPosition: Int): Int { + return roundDownToBlockSize(trialPosition + blockSize - 1) + } + + override fun createScroller(layoutManager: RecyclerView.LayoutManager): RecyclerView.SmoothScroller? { + return if (layoutManager !is ScrollVectorProvider) { + null + } else object : LinearSmoothScroller(recyclerView.context) { + override fun onTargetFound(targetView: View, state: RecyclerView.State, action: Action) { + val snapDistances = calculateDistanceToFinalSnap( + recyclerView.layoutManager!!, + targetView, + ) + val dx = snapDistances[0] + val dy = snapDistances[1] + val time = calculateTimeForDeceleration( + max(abs(dx.toDouble()), abs(dy.toDouble())) + .toInt(), + ) + if (time > 0) { + action.update(dx, dy, time, snapInterpolator) + } + } + + override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float { + return 40f / displayMetrics.densityDpi + } + } + } + + /* + Helper class that handles calculations for LTR and RTL layouts. + */ + private inner class LayoutDirectionHelper(direction: Int) { + + // Is the layout an RTL one? + private val mIsRTL: Boolean + + init { + mIsRTL = direction == View.LAYOUT_DIRECTION_RTL + } + + /* + Calculate the amount of scroll needed to align the target view with the layout edge. + */ + fun getScrollToAlignView(targetView: View?): Int { + return if (mIsRTL) orientationHelper.getDecoratedEnd(targetView) - recyclerView.width else orientationHelper.getDecoratedStart( + targetView, + ) + } + + /** + * Calculate the distance to final snap position when the view corresponding to the snap + * position is not currently available. + * + * @param layoutManager LinearLayoutManager or descendant class + * @param targetPos - Adapter position to snap to + * @return int[2] {x-distance in pixels, y-distance in pixels} + */ + fun calculateDistanceToScroll(layoutManager: LinearLayoutManager, targetPos: Int): IntArray { + val out = IntArray(2) + val firstVisiblePos = layoutManager.findFirstVisibleItemPosition() + if (layoutManager.canScrollHorizontally()) { + if (targetPos <= firstVisiblePos) { // scrolling toward top of data + if (mIsRTL) { + val lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition()) + out[0] = (orientationHelper.getDecoratedEnd(lastView) + + (firstVisiblePos - targetPos) * itemDimension) + } else { + val firstView = layoutManager.findViewByPosition(firstVisiblePos) + out[0] = (orientationHelper.getDecoratedStart(firstView) + - (firstVisiblePos - targetPos) * itemDimension) + } + } + } + if (layoutManager.canScrollVertically()) { + if (targetPos <= firstVisiblePos) { // scrolling toward top of data + val firstView = layoutManager.findViewByPosition(firstVisiblePos) + out[1] = firstView!!.top - (firstVisiblePos - targetPos) * itemDimension + } + } + return out + } + + /* + Calculate the number of positions to move in the RecyclerView given a scroll amount + and the size of the items to be scrolled. Return integral multiple of mBlockSize not + equal to zero. + */ + fun getPositionsToMove(llm: LinearLayoutManager, scroll: Int, itemSize: Int): Int { + var positionsToMove: Int + positionsToMove = roundUpToBlockSize(abs((scroll.toDouble()) / itemSize).roundToInt()) + if (positionsToMove < blockSize) { + // Must move at least one block + positionsToMove = blockSize + } else if (positionsToMove > maxPositionsToMove) { + // Clamp number of positions to move, so we don't get wild flinging. + positionsToMove = maxPositionsToMove + } + if (scroll < 0) { + positionsToMove *= -1 + } + if (mIsRTL) { + positionsToMove *= -1 + } + return if (layoutDirectionHelper.isDirectionToBottom(scroll < 0)) { + // Scrolling toward the bottom of data. + roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionsToMove + } else roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove + // Scrolling toward the top of the data. + } + + fun isDirectionToBottom(velocityNegative: Boolean): Boolean { + return if (mIsRTL) velocityNegative else !velocityNegative + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePagesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePagesAdapter.kt new file mode 100644 index 000000000..f6384ad09 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoublePagesAdapter.kt @@ -0,0 +1,35 @@ +package org.koitharu.kotatsu.reader.ui.pager.doublepage + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner +import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver +import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.databinding.ItemPageBinding +import org.koitharu.kotatsu.reader.domain.PageLoader +import org.koitharu.kotatsu.reader.ui.config.ReaderSettings +import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter + +class DoublePagesAdapter( + private val lifecycleOwner: LifecycleOwner, + loader: PageLoader, + settings: ReaderSettings, + networkState: NetworkState, + exceptionResolver: ExceptionResolver, +) : BaseReaderAdapter(loader, settings, networkState, exceptionResolver) { + + override fun onCreateViewHolder( + parent: ViewGroup, + loader: PageLoader, + settings: ReaderSettings, + networkState: NetworkState, + exceptionResolver: ExceptionResolver, + ) = DoublePageHolder( + owner = lifecycleOwner, + binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false), + loader = loader, + settings = settings, + networkState = networkState, + exceptionResolver = exceptionResolver, + ) +} diff --git a/app/src/main/res/layout/fragment_reader_double.xml b/app/src/main/res/layout/fragment_reader_double.xml new file mode 100644 index 000000000..87aa88704 --- /dev/null +++ b/app/src/main/res/layout/fragment_reader_double.xml @@ -0,0 +1,10 @@ + +