Double reader integration
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -6,7 +6,6 @@ enum class ReaderMode(val id: Int) {
|
||||
REVERSED(3),
|
||||
VERTICAL(4),
|
||||
WEBTOON(2),
|
||||
DOUBLE(5),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<out BaseReaderFragment<*>>>(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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<FragmentReaderPagerB
|
||||
|
||||
viewModel.pageAnimation.observe(viewLifecycleOwner) {
|
||||
val transformer = when (it) {
|
||||
ReaderAnimation.NONE -> NoAnimPageTransformer()
|
||||
ReaderAnimation.NONE -> NoAnimPageTransformer(binding.pager.orientation)
|
||||
ReaderAnimation.DEFAULT -> null
|
||||
ReaderAnimation.ADVANCED -> onCreateAdvancedTransformer()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FragmentReaderDoubleBinding>() {
|
||||
@Inject
|
||||
lateinit var pageLoader: PageLoader
|
||||
|
||||
private var recyclerLifecycleDispatcher: RecyclerViewLifecycleDispatcher? = null
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -46,12 +49,16 @@ class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding>() {
|
||||
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<FragmentReaderDoubleBinding>() {
|
||||
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<FragmentReaderDoubleBinding>() {
|
||||
}
|
||||
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<FragmentReaderDoubleBinding>() {
|
||||
)
|
||||
|
||||
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<FragmentReaderDoubleBinding>() {
|
||||
)
|
||||
}
|
||||
|
||||
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<PageHolder> {
|
||||
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,
|
||||
|
||||
@@ -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<PageHolder> {
|
||||
val lm = layoutManager as? LinearLayoutManager ?: return emptySequence()
|
||||
return (lm.findFirstVisibleItemPosition()..lm.findLastVisibleItemPosition()).asSequence()
|
||||
.mapNotNull { findViewHolderForAdapterPosition(it) as? PageHolder }
|
||||
}
|
||||
|
||||
fun RecyclerView.allPageHolders(): Sequence<PageHolder> {
|
||||
return children.mapNotNull {
|
||||
findContainingViewHolder(it) as? PageHolder
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
app/src/main/res/drawable/ic_split_horizontal.xml
Normal file
22
app/src/main/res/drawable/ic_split_horizontal.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4,4C2.89,4 2,4.89 2,6L2,18C2,19.11 2.89,20 4,20L9,20L9,18L8,18L6,18L4,18L4,6L6,6L8,6L9,6L9,4L4,4zM15,4L15,6L16,6L18,6L20,6L20,18L18,18L16,18L15,18L15,20L20,20C21.1,20 22,19.11 22,18L22,6C22,4.89 21.1,4 20,4L15,4z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m11,21l-0,-3l2,0l-0,3z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m13,3l-0,3l-2,0l-0,-3l2,0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m11,16l-0,-3l2,0l-0,3l-2,0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m11,11l-0,-3l2,0l-0,3l-2,0" />
|
||||
</vector>
|
||||
@@ -119,15 +119,6 @@
|
||||
android:text="@string/webtoon"
|
||||
app:icon="@drawable/ic_script" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_double"
|
||||
style="@style/Widget.Kotatsu.ToggleButton.Vertical"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/two_pages"
|
||||
app:icon="@drawable/ic_pager_double_ltr" />
|
||||
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<TextView
|
||||
@@ -139,7 +130,7 @@
|
||||
android:textAppearance="?attr/textAppearanceBodySmall" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switch_scroll_timer"
|
||||
android:id="@+id/switch_double_reader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
@@ -148,6 +139,20 @@
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:singleLine="true"
|
||||
android:text="@string/use_two_pages_landscape"
|
||||
android:textAppearance="?attr/textAppearanceButton"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
app:drawableStartCompat="@drawable/ic_split_horizontal" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switch_scroll_timer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:ellipsize="end"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:singleLine="true"
|
||||
android:text="@string/automatic_scroll"
|
||||
android:textAppearance="?attr/textAppearanceButton"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
|
||||
@@ -582,4 +582,5 @@
|
||||
<string name="long_tap_action">Long tap action</string>
|
||||
<string name="none">None</string>
|
||||
<string name="config_reset_confirm">Reset settings to default values? This action cannot be undone.</string>
|
||||
<string name="use_two_pages_landscape">Use two pages layout on landscape orientation (beta)</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user