feat(reader): Add sensitivity setting for double-page mode

This commit introduces a new setting to control the scroll sensitivity of the double-page reader mode.

- A SeekBar has been added to the reader configuration sheet to adjust the sensitivity.
- The DoublePageSnapHelper now uses this setting to calculate the scroll distance.
- The setting is stored in AppSettings.
This commit is contained in:
puargs
2025-09-04 22:40:08 -06:00
parent f7a461a9d8
commit 1d1e49123a
6 changed files with 70 additions and 20 deletions

View File

@@ -138,6 +138,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_READER_DOUBLE_PAGES, false)
set(value) = prefs.edit { putBoolean(KEY_READER_DOUBLE_PAGES, value) }
val readerDoublePagesSensitivity: Float
get() = prefs.getFloat(KEY_READER_DOUBLE_PAGES_SENSITIVITY, 12f) / 10f
fun setReaderDoublePagesSensitivity(value: Float) {
prefs.edit { putFloat(KEY_READER_DOUBLE_PAGES_SENSITIVITY, value) }
}
val readerScreenOrientation: Int
get() = prefs.getString(KEY_READER_ORIENTATION, null)?.toIntOrNull()
?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
@@ -669,6 +676,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
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_DOUBLE_PAGES_SENSITIVITY = "reader_double_pages_sensitivity"
const val KEY_READER_ZOOM_BUTTONS = "reader_zoom_buttons"
const val KEY_READER_CONTROL_LTR = "reader_taps_ltr"
const val KEY_READER_NAVIGATION_INVERTED = "reader_navigation_inverted"

View File

@@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.SeekBar
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
@@ -87,6 +88,10 @@ class ReaderConfigSheet :
binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape
binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD || mode == ReaderMode.REVERSED
binding.textSensitivity.isVisible = settings.isReaderDoubleOnLandscape
binding.seekbarSensitivity.isVisible = settings.isReaderDoubleOnLandscape
binding.seekbarSensitivity.progress = (settings.readerDoublePagesSensitivity * 100).toInt()
binding.checkableGroup.addOnButtonCheckedListener(this)
binding.buttonSavePage.setOnClickListener(this)
binding.buttonScreenRotate.setOnClickListener(this)
@@ -97,6 +102,16 @@ class ReaderConfigSheet :
binding.buttonBookmark.setOnClickListener(this)
binding.switchDoubleReader.setOnCheckedChangeListener(this)
binding.seekbarSensitivity.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
settings.setReaderDoublePagesSensitivity(progress / 10f)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
viewModel.isBookmarkAdded.observe(viewLifecycleOwner) {
binding.buttonBookmark.setText(if (it) R.string.bookmark_remove else R.string.bookmark_add)
binding.buttonBookmark.setCompoundDrawablesRelativeWithIntrinsicBounds(
@@ -170,6 +185,8 @@ class ReaderConfigSheet :
R.id.switch_double_reader -> {
settings.isReaderDoubleOnLandscape = isChecked
viewBinding?.textSensitivity?.isVisible = isChecked
viewBinding?.seekbarSensitivity?.isVisible = isChecked
findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked)
}
}

View File

@@ -11,11 +11,14 @@ import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider
import androidx.recyclerview.widget.SnapHelper
import org.koitharu.kotatsu.core.prefs.AppSettings
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.max
import kotlin.math.roundToInt
import kotlin.math.sign
class DoublePageSnapHelper : SnapHelper() {
class DoublePageSnapHelper(private val settings: AppSettings) : SnapHelper() {
private lateinit var recyclerView: RecyclerView
@@ -248,28 +251,27 @@ class DoublePageSnapHelper : SnapHelper() {
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
val sensitivity = settings.readerDoublePagesSensitivity
var positionsToMove = (scroll.toDouble() / (itemSize * (2.5 - sensitivity))).roundToInt()
// Apply a maximum threshold
val maxPages = (4 * sensitivity).roundToInt().coerceAtLeast(1)
if (positionsToMove.absoluteValue > maxPages) {
positionsToMove = maxPages * positionsToMove.sign
}
if (scroll < 0) {
positionsToMove *= -1
// Apply a minimum threshold
if (positionsToMove == 0 && scroll.absoluteValue > itemSize * 0.2) {
positionsToMove = 1 * scroll.sign
}
if (isRTL) {
positionsToMove *= -1
}
return if (layoutDirectionHelper.isDirectionToBottom(scroll < 0)) {
// Scrolling toward the bottom of data.
roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionsToMove
val currentPosition = if (layoutDirectionHelper.isDirectionToBottom(scroll < 0)) {
llm.findFirstVisibleItemPosition()
} else {
roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove
llm.findLastVisibleItemPosition()
}
// Scrolling toward the top of the data.
val targetPos = currentPosition + positionsToMove * 2
return roundDownToBlockSize(targetPos)
}
fun isDirectionToBottom(velocityNegative: Boolean): Boolean {

View File

@@ -13,6 +13,7 @@ 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.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.list.lifecycle.RecyclerViewLifecycleDispatcher
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.databinding.FragmentReaderDoubleBinding
@@ -33,6 +34,9 @@ open class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding
@Inject
lateinit var pageLoader: PageLoader
@Inject
lateinit var settings: AppSettings
private var recyclerLifecycleDispatcher: RecyclerViewLifecycleDispatcher? = null
override fun onCreateViewBinding(
@@ -51,7 +55,7 @@ open class DoubleReaderFragment : BaseReaderFragment<FragmentReaderDoubleBinding
addOnScrollListener(it)
}
addOnScrollListener(PageScrollListener())
DoublePageSnapHelper().attachToRecyclerView(this)
DoublePageSnapHelper(settings).attachToRecyclerView(this)
}
}

View File

@@ -129,6 +129,24 @@
android:textColor="?colorOnSurfaceVariant"
app:drawableStartCompat="@drawable/ic_split_horizontal" />
<TextView
android:id="@+id/text_sensitivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_small"
android:text="@string/two_page_scroll_sensitivity"
android:textAppearance="@style/TextAppearance.Kotatsu.GridTitle" />
<SeekBar
android:id="@+id/seekbar_sensitivity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_small"
android:max="100"
tools:progress="50" />
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
android:id="@+id/button_screen_rotate"
android:layout_width="match_parent"

View File

@@ -572,6 +572,7 @@
<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>
<string name="two_page_scroll_sensitivity">Two-Page Scroll Sensitivity</string>
<string name="default_webtoon_zoom_out">Default webtoon zoom out</string>
<string name="fullscreen_mode">Fullscreen mode</string>
<string name="reader_fullscreen_summary">Hide system status and navigation bars</string>