diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index ac50c83e7..75d184136 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -101,6 +101,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { } } + var isReaderDoubleOnLandscape: Boolean + get() = prefs.getBoolean(KEY_READER_DOUBLE_PAGES, false) + set(value) = prefs.edit { putBoolean(KEY_READER_DOUBLE_PAGES, value) } + val isReaderVolumeButtonsEnabled: Boolean get() = prefs.getBoolean(KEY_READER_VOLUME_BUTTONS, false) @@ -472,6 +476,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_GRID_SIZE = "grid_size" const val KEY_REMOTE_SOURCES = "remote_sources" const val KEY_LOCAL_STORAGE = "local_storage" + const val KEY_READER_DOUBLE_PAGES = "reader_double_pages" const val KEY_READER_ZOOM_BUTTONS = "reader_zoom_buttons" const val KEY_READER_VOLUME_BUTTONS = "reader_volume_buttons" const val KEY_TRACKER_ENABLED = "tracker_enabled" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ReaderMode.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ReaderMode.kt index 2b727c6c5..7d9f5fd2f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ReaderMode.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/ReaderMode.kt @@ -6,7 +6,6 @@ enum class ReaderMode(val id: Int) { REVERSED(3), VERTICAL(4), WEBTOON(2), - DOUBLE(5), ; companion object { 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 7c444d428..2808be2b3 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 @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build.VERSION_CODES.R import android.os.Bundle import android.transition.Fade import android.transition.Slide @@ -108,7 +109,7 @@ class ReaderActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityReaderBinding.inflate(layoutInflater)) - readerManager = ReaderManager(supportFragmentManager, viewBinding.container) + readerManager = ReaderManager(supportFragmentManager, viewBinding.container, settings) supportActionBar?.setDisplayHomeAsUpEnabled(true) touchHelper = TapGridDispatcher(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 0d6b125fb..9e57f626c 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,9 +1,10 @@ package org.koitharu.kotatsu.reader.ui +import android.content.res.Configuration 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.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment import org.koitharu.kotatsu.reader.ui.pager.doublepage.DoubleReaderFragment @@ -16,13 +17,13 @@ import java.util.EnumMap class ReaderManager( private val fragmentManager: FragmentManager, private val container: FragmentContainerView, + private val settings: AppSettings, ) { private val modeMap = EnumMap>>(ReaderMode::class.java) init { - val isTablet = container.resources.getBoolean(R.bool.is_tablet) - modeMap[ReaderMode.STANDARD] = if (isTablet) { + modeMap[ReaderMode.STANDARD] = if (useDoublePages()) { DoubleReaderFragment::class.java } else { PagerReaderFragment::class.java @@ -49,6 +50,9 @@ class ReaderManager( } } + private fun useDoublePages() = container.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + && settings.isReaderDoubleOnLandscape + /*fun replace(reader: BaseReaderFragment<*>) { fragmentManager.commit { setReorderingAllowed(true) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt index 2cdbb7e29..d13371235 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.reader.ui.config import android.net.Uri +import android.os.Build.VERSION_CODES.R import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -80,7 +81,8 @@ class ReaderConfigSheet : binding.buttonReversed.isChecked = mode == ReaderMode.REVERSED binding.buttonWebtoon.isChecked = mode == ReaderMode.WEBTOON binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL - binding.buttonDouble.isChecked = mode == ReaderMode.DOUBLE + binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape + binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD binding.checkableGroup.addOnButtonCheckedListener(this) binding.buttonSavePage.setOnClickListener(this) @@ -89,6 +91,7 @@ class ReaderConfigSheet : binding.buttonColorFilter.setOnClickListener(this) binding.sliderTimer.addOnChangeListener(this) binding.switchScrollTimer.setOnCheckedChangeListener(this) + binding.switchDoubleReader.setOnCheckedChangeListener(this) settings.observeAsStateFlow( scope = lifecycleScope + Dispatchers.Default, @@ -140,6 +143,11 @@ class ReaderConfigSheet : R.id.switch_screen_lock_rotation -> { orientationHelper.isLocked = isChecked } + + R.id.switch_double_reader -> { + settings.isReaderDoubleOnLandscape = isChecked + findCallback()?.onReaderModeChanged(mode) + } } } @@ -156,9 +164,9 @@ class ReaderConfigSheet : R.id.button_webtoon -> ReaderMode.WEBTOON R.id.button_reversed -> ReaderMode.REVERSED R.id.button_vertical -> ReaderMode.VERTICAL - R.id.button_double -> ReaderMode.DOUBLE else -> return } + viewBinding?.switchDoubleReader?.isEnabled = newMode == ReaderMode.STANDARD if (newMode == mode) { return } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePagerReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePagerReaderFragment.kt index 1a697cb36..ff4bf2961 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePagerReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePagerReaderFragment.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.reader.ui.pager import android.os.Build +import android.os.Build.VERSION_CODES.R import android.os.Bundle import android.view.InputDevice import android.view.KeyEvent @@ -75,7 +76,7 @@ abstract class BasePagerReaderFragment : BaseReaderFragment NoAnimPageTransformer() + ReaderAnimation.NONE -> NoAnimPageTransformer(binding.pager.orientation) ReaderAnimation.DEFAULT -> null ReaderAnimation.ADVANCED -> onCreateAdvancedTransformer() } 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 index a2930fa53..d66864e30 100644 --- 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 @@ -16,4 +16,10 @@ class DoublePageLayoutManager( lp?.width = width / 2 return super.checkLayoutParams(lp) } + + override fun calculateExtraLayoutSpace(state: RecyclerView.State, extraLayoutSpace: IntArray) { + val offscreenSpace = width / 2 + extraLayoutSpace[0] = offscreenSpace + extraLayoutSpace[1] = offscreenSpace + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoubleReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoubleReaderFragment.kt index 05b0e210e..1a55a3994 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoubleReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/DoubleReaderFragment.kt @@ -1,8 +1,10 @@ package org.koitharu.kotatsu.reader.ui.pager.doublepage +import android.os.Build.VERSION_CODES.R import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup +import android.view.animation.AccelerateDecelerateInterpolator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar @@ -12,16 +14,15 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.yield import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.os.NetworkState +import org.koitharu.kotatsu.core.ui.list.lifecycle.RecyclerViewLifecycleDispatcher 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.ReaderViewModel 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 org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder import javax.inject.Inject import kotlin.math.absoluteValue @@ -34,6 +35,8 @@ class DoubleReaderFragment : BaseReaderFragment() { @Inject lateinit var pageLoader: PageLoader + private var recyclerLifecycleDispatcher: RecyclerViewLifecycleDispatcher? = null + override fun onCreateViewBinding( inflater: LayoutInflater, container: ViewGroup?, @@ -46,12 +49,16 @@ class DoubleReaderFragment : BaseReaderFragment() { super.onViewBindingCreated(binding, savedInstanceState) with(binding.recyclerView) { adapter = readerAdapter + recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also { + addOnScrollListener(it) + } addOnScrollListener(PageScrollListener(viewModel)) DoublePageSnapHelper().attachToRecyclerView(this) } } override fun onDestroyView() { + recyclerLifecycleDispatcher = null requireViewBinding().recyclerView.adapter = null super.onDestroyView() } @@ -60,6 +67,9 @@ class DoubleReaderFragment : BaseReaderFragment() { val items = launch { requireAdapter().setItems(pages) yield() + viewBinding?.recyclerView?.let { rv -> + recyclerLifecycleDispatcher?.invalidate(rv) + } } if (pendingState != null) { var position = pages.indexOfFirst { @@ -67,7 +77,7 @@ class DoubleReaderFragment : BaseReaderFragment() { } items.join() if (position != -1) { - position = position or 1 + position = position.toPagePosition() requireViewBinding().recyclerView.firstVisibleItemPosition = position viewModel.onCurrentPageChanged(position, position + 1) } else { @@ -88,26 +98,34 @@ class DoubleReaderFragment : BaseReaderFragment() { ) override fun onZoomIn() { - (viewBinding ?: return).recyclerView.pageHolders() + (viewBinding ?: return).recyclerView.visiblePageHolders() .forEach { it.onZoomIn() } } override fun onZoomOut() { - (viewBinding ?: return).recyclerView.pageHolders() + (viewBinding ?: return).recyclerView.visiblePageHolders() .forEach { it.onZoomOut() } } override fun switchPageBy(delta: Int) { - switchPageTo((requireViewBinding().recyclerView.currentItem() + delta) or 1, delta.absoluteValue > 1) + if (delta.absoluteValue > 1 || !isAnimationEnabled()) { + switchPageTo(getCurrentItem() + delta + delta, false) + return + } + val rv = viewBinding?.recyclerView ?: return + val distance = rv.width * delta + rv.smoothScrollBy(distance, 0, AccelerateDecelerateInterpolator()) } override fun switchPageTo(position: Int, smooth: Boolean) { - requireViewBinding().recyclerView.firstVisibleItemPosition = position or 1 + val lm = viewBinding?.recyclerView?.layoutManager as? LinearLayoutManager ?: return + val targetPosition = position.toPagePosition() + lm.scrollToPositionWithOffset(targetPosition, 0) } override fun getCurrentState(): ReaderState? = viewBinding?.run { val adapter = recyclerView.adapter as? BaseReaderAdapter<*> - val page = adapter?.getItemOrNull(recyclerView.currentItem()) ?: return@run null + val page = adapter?.getItemOrNull(getCurrentItem()) ?: return@run null ReaderState( chapterId = page.chapterId, page = page.index, @@ -115,16 +133,10 @@ class DoubleReaderFragment : BaseReaderFragment() { ) } - private fun RecyclerView.currentItem(): Int { - val lm = layoutManager as LinearLayoutManager - return ((lm.findFirstVisibleItemPosition() + lm.findLastVisibleItemPosition()) / 2f).toIntUp() - } + private fun getCurrentItem() = (requireViewBinding().recyclerView.layoutManager as LinearLayoutManager) + .findFirstCompletelyVisibleItemPosition().toPagePosition() - private fun RecyclerView.pageHolders(): Sequence { - val lm = layoutManager as? LinearLayoutManager ?: return emptySequence() - return (lm.findFirstVisibleItemPosition()..lm.findLastVisibleItemPosition()).asSequence() - .mapNotNull { findViewHolderForAdapterPosition(it) as? PageHolder } - } + private fun Int.toPagePosition() = this and 1.inv() private class PageScrollListener( private val viewModel: ReaderViewModel, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/Utils.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/Utils.kt new file mode 100644 index 000000000..3efd84ce7 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/doublepage/Utils.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.reader.ui.pager.doublepage + +import androidx.core.view.children +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder + +fun RecyclerView.visiblePageHolders(): Sequence { + val lm = layoutManager as? LinearLayoutManager ?: return emptySequence() + return (lm.findFirstVisibleItemPosition()..lm.findLastVisibleItemPosition()).asSequence() + .mapNotNull { findViewHolderForAdapterPosition(it) as? PageHolder } +} + +fun RecyclerView.allPageHolders(): Sequence { + return children.mapNotNull { + findContainingViewHolder(it) as? PageHolder + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/NoAnimPageTransformer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/NoAnimPageTransformer.kt index 2ce4317a1..a78d0d8a8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/NoAnimPageTransformer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/NoAnimPageTransformer.kt @@ -3,13 +3,22 @@ package org.koitharu.kotatsu.reader.ui.pager.standard import android.view.View import androidx.viewpager2.widget.ViewPager2 -class NoAnimPageTransformer : ViewPager2.PageTransformer { +class NoAnimPageTransformer( + private val orientation: Int +) : ViewPager2.PageTransformer { override fun transformPage(page: View, position: Float) { page.translationX = when { + orientation != ViewPager2.ORIENTATION_HORIZONTAL -> 0f position in -0.5f..0.5f -> -position * page.width.toFloat() position > 0 -> page.width.toFloat() else -> -page.width.toFloat() } + page.translationY = when { + orientation != ViewPager2.ORIENTATION_VERTICAL -> 0f + position in -0.5f..0.5f -> -position * page.height.toFloat() + position > 0 -> page.height.toFloat() + else -> -page.height.toFloat() + } } } diff --git a/app/src/main/res/drawable/ic_split_horizontal.xml b/app/src/main/res/drawable/ic_split_horizontal.xml new file mode 100644 index 000000000..2bb2bede9 --- /dev/null +++ b/app/src/main/res/drawable/ic_split_horizontal.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/layout/sheet_reader_config.xml b/app/src/main/res/layout/sheet_reader_config.xml index 37165765f..ff3ecadfb 100644 --- a/app/src/main/res/layout/sheet_reader_config.xml +++ b/app/src/main/res/layout/sheet_reader_config.xml @@ -119,15 +119,6 @@ android:text="@string/webtoon" app:icon="@drawable/ic_script" /> - - + + Long tap action None Reset settings to default values? This action cannot be undone. + Use two pages layout on landscape orientation (beta)