Compare commits
1 Commits
v9.0.1
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5df55d1fe9 |
@@ -13,11 +13,11 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
|||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class ReaderActivity :
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(ActivityReaderBinding.inflate(layoutInflater))
|
setContentView(ActivityReaderBinding.inflate(layoutInflater))
|
||||||
readerManager = ReaderManager(supportFragmentManager, R.id.container)
|
readerManager = ReaderManager(supportFragmentManager, viewBinding.container)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
touchHelper = GridTouchHelper(this, this)
|
touchHelper = GridTouchHelper(this, this)
|
||||||
scrollTimer = scrollTimerFactory.create(this, this)
|
scrollTimer = scrollTimerFactory.create(this, this)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
import androidx.annotation.IdRes
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
|
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.reversed.ReversedReaderFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
|
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonReaderFragment
|
||||||
@@ -12,19 +14,24 @@ import java.util.EnumMap
|
|||||||
|
|
||||||
class ReaderManager(
|
class ReaderManager(
|
||||||
private val fragmentManager: FragmentManager,
|
private val fragmentManager: FragmentManager,
|
||||||
@IdRes private val containerResId: Int,
|
private val container: FragmentContainerView,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val modeMap = EnumMap<ReaderMode, Class<out BaseReaderFragment<*>>>(ReaderMode::class.java)
|
private val modeMap = EnumMap<ReaderMode, Class<out BaseReaderFragment<*>>>(ReaderMode::class.java)
|
||||||
|
|
||||||
init {
|
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.REVERSED] = ReversedReaderFragment::class.java
|
||||||
modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java
|
modeMap[ReaderMode.WEBTOON] = WebtoonReaderFragment::class.java
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentReader: BaseReaderFragment<*>?
|
val currentReader: BaseReaderFragment<*>?
|
||||||
get() = fragmentManager.findFragmentById(containerResId) as? BaseReaderFragment<*>
|
get() = fragmentManager.findFragmentById(container.id) as? BaseReaderFragment<*>
|
||||||
|
|
||||||
val currentMode: ReaderMode?
|
val currentMode: ReaderMode?
|
||||||
get() {
|
get() {
|
||||||
@@ -36,14 +43,14 @@ class ReaderManager(
|
|||||||
val readerClass = requireNotNull(modeMap[newMode])
|
val readerClass = requireNotNull(modeMap[newMode])
|
||||||
fragmentManager.commit {
|
fragmentManager.commit {
|
||||||
setReorderingAllowed(true)
|
setReorderingAllowed(true)
|
||||||
replace(containerResId, readerClass, null, null)
|
replace(container.id, readerClass, null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replace(reader: BaseReaderFragment<*>) {
|
/*fun replace(reader: BaseReaderFragment<*>) {
|
||||||
fragmentManager.commit {
|
fragmentManager.commit {
|
||||||
setReorderingAllowed(true)
|
setReorderingAllowed(true)
|
||||||
replace(containerResId, reader)
|
replace(containerResId, reader)
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<FragmentReaderDoubleBinding>() {
|
||||||
|
|
||||||
|
@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<ReaderPage>, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<DoublePageHolder>(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,
|
||||||
|
)
|
||||||
|
}
|
||||||
10
app/src/main/res/layout/fragment_reader_double.xml
Normal file
10
app/src/main/res/layout/fragment_reader_double.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/recyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:defaultFocusHighlightEnabled="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.doublepage.DoublePageLayoutManager" />
|
||||||
Reference in New Issue
Block a user