feat: Add Pull Gesture Navigate Chapter
This commit is contained in:
26
.idea/appInsightsSettings.xml
generated
Normal file
26
.idea/appInsightsSettings.xml
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AppInsightsSettings">
|
||||
<option name="tabSettings">
|
||||
<map>
|
||||
<entry key="Firebase Crashlytics">
|
||||
<value>
|
||||
<InsightsFilterSettings>
|
||||
<option name="connection">
|
||||
<ConnectionSetting>
|
||||
<option name="appId" value="PLACEHOLDER" />
|
||||
<option name="mobileSdkAppId" value="" />
|
||||
<option name="projectId" value="" />
|
||||
<option name="projectNumber" value="" />
|
||||
</ConnectionSetting>
|
||||
</option>
|
||||
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||
<option name="visibilityType" value="ALL" />
|
||||
</InsightsFilterSettings>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -488,6 +488,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
get() = prefs.getBoolean(KEY_WEBTOON_GAPS, false)
|
||||
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_GAPS, value) }
|
||||
|
||||
var isWebtoonPullGestureEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_WEBTOON_PULL_GESTURE, false)
|
||||
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_PULL_GESTURE, value) }
|
||||
|
||||
@get:FloatRange(from = 0.0, to = 0.5)
|
||||
val defaultWebtoonZoomOut: Float
|
||||
get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f
|
||||
@@ -748,6 +752,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_WEBTOON_GAPS = "webtoon_gaps"
|
||||
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
||||
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
|
||||
const val KEY_WEBTOON_PULL_GESTURE = "webtoon_pull_gesture"
|
||||
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
||||
const val KEY_APP_LOCALE = "app_locale"
|
||||
const val KEY_SOURCES_GRID = "sources_grid"
|
||||
|
||||
@@ -157,6 +157,12 @@ class ReaderViewModel @Inject constructor(
|
||||
valueProducer = { isWebtoonGapsEnabled },
|
||||
)
|
||||
|
||||
val isWebtoonPullGestureEnabled = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_WEBTOON_PULL_GESTURE,
|
||||
valueProducer = { isWebtoonPullGestureEnabled },
|
||||
)
|
||||
|
||||
val defaultWebtoonZoomOut = observeIsWebtoonZoomEnabled().flatMapLatest {
|
||||
if (it) {
|
||||
observeWebtoonZoomOut()
|
||||
@@ -345,11 +351,14 @@ class ReaderViewModel @Inject constructor(
|
||||
return@launchJob
|
||||
}
|
||||
ensureActive()
|
||||
if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
||||
}
|
||||
if (lowerPos <= BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
||||
val autoLoadAllowed = readerMode.value != ReaderMode.WEBTOON || !isWebtoonPullGestureEnabled.value
|
||||
if (autoLoadAllowed) {
|
||||
if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
||||
}
|
||||
if (lowerPos <= BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
||||
}
|
||||
}
|
||||
if (pageLoader.isPrefetchApplicable()) {
|
||||
pageLoader.prefetch(pages.trySublist(upperPos + 1, upperPos + PREFETCH_LIMIT))
|
||||
|
||||
@@ -86,6 +86,8 @@ class ReaderConfigSheet :
|
||||
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
|
||||
binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape
|
||||
binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD || mode == ReaderMode.REVERSED
|
||||
binding.switchPullGesture.isChecked = settings.isWebtoonPullGestureEnabled
|
||||
binding.switchPullGesture.isEnabled = mode == ReaderMode.WEBTOON
|
||||
|
||||
binding.checkableGroup.addOnButtonCheckedListener(this)
|
||||
binding.buttonSavePage.setOnClickListener(this)
|
||||
@@ -96,6 +98,7 @@ class ReaderConfigSheet :
|
||||
binding.buttonScrollTimer.setOnClickListener(this)
|
||||
binding.buttonBookmark.setOnClickListener(this)
|
||||
binding.switchDoubleReader.setOnCheckedChangeListener(this)
|
||||
binding.switchPullGesture.setOnCheckedChangeListener(this)
|
||||
|
||||
viewModel.isBookmarkAdded.observe(viewLifecycleOwner) {
|
||||
binding.buttonBookmark.setText(if (it) R.string.bookmark_remove else R.string.bookmark_add)
|
||||
@@ -172,6 +175,10 @@ class ReaderConfigSheet :
|
||||
settings.isReaderDoubleOnLandscape = isChecked
|
||||
findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked)
|
||||
}
|
||||
|
||||
R.id.switch_pull_gesture -> {
|
||||
settings.isWebtoonPullGestureEnabled = isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +198,7 @@ class ReaderConfigSheet :
|
||||
else -> return
|
||||
}
|
||||
viewBinding?.switchDoubleReader?.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED
|
||||
viewBinding?.switchPullGesture?.isEnabled = newMode == ReaderMode.WEBTOON
|
||||
if (newMode == mode) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -46,6 +47,8 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentReaderWebtoonBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
var canGoPrev = true
|
||||
var canGoNext = true
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = readerAdapter
|
||||
@@ -53,6 +56,40 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also {
|
||||
addOnScrollListener(it)
|
||||
}
|
||||
setOnPullGestureListener(object : WebtoonRecyclerView.OnPullGestureListener {
|
||||
override fun onPullProgressTop(progress: Float) {
|
||||
if (canGoPrev) {
|
||||
binding.feedbackTop.setText(R.string.pull_to_prev_chapter)
|
||||
} else {
|
||||
binding.feedbackTop.setText(R.string.pull_top_no_prev)
|
||||
}
|
||||
updateFeedback(binding.feedbackTop, progress)
|
||||
}
|
||||
override fun onPullProgressBottom(progress: Float) {
|
||||
if (canGoNext) {
|
||||
binding.feedbackBottom.setText(R.string.pull_to_next_chapter)
|
||||
} else {
|
||||
binding.feedbackBottom.setText(R.string.pull_bottom_no_next)
|
||||
}
|
||||
updateFeedback(binding.feedbackBottom, progress)
|
||||
}
|
||||
override fun onPullTriggeredTop() {
|
||||
fadeOut(binding.feedbackTop)
|
||||
if (canGoPrev) {
|
||||
viewModel.switchChapterBy(-1)
|
||||
}
|
||||
}
|
||||
override fun onPullTriggeredBottom() {
|
||||
fadeOut(binding.feedbackBottom)
|
||||
if (canGoNext) {
|
||||
viewModel.switchChapterBy(1)
|
||||
}
|
||||
}
|
||||
override fun onPullCancelled() {
|
||||
fadeOut(binding.feedbackTop)
|
||||
fadeOut(binding.feedbackBottom)
|
||||
}
|
||||
})
|
||||
}
|
||||
viewModel.isWebtoonZooEnabled.observe(viewLifecycleOwner) {
|
||||
binding.frame.isZoomEnable = it
|
||||
@@ -70,6 +107,21 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
viewModel.readerSettingsProducer.observe(viewLifecycleOwner) {
|
||||
it.applyBackground(binding.root)
|
||||
}
|
||||
viewModel.readerMode.observe(viewLifecycleOwner) { mode ->
|
||||
binding.recyclerView.isPullGestureEnabled = (mode == org.koitharu.kotatsu.core.prefs.ReaderMode.WEBTOON) && viewModel.isWebtoonPullGestureEnabled.value
|
||||
}
|
||||
viewModel.isWebtoonPullGestureEnabled.observe(viewLifecycleOwner) { enabled ->
|
||||
binding.recyclerView.isPullGestureEnabled = (viewModel.readerMode.value == org.koitharu.kotatsu.core.prefs.ReaderMode.WEBTOON) && enabled
|
||||
}
|
||||
viewModel.uiState.observe(viewLifecycleOwner) { state ->
|
||||
if (state != null) {
|
||||
canGoPrev = state.chapterIndex > 0
|
||||
canGoNext = state.chapterIndex < state.chaptersTotal - 1
|
||||
} else {
|
||||
canGoPrev = true
|
||||
canGoNext = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -178,3 +230,14 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
return getChildAdapterPosition(view)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFeedback(tv: TextView, progress: Float) {
|
||||
val clamped = progress.coerceIn(0f, 1.2f)
|
||||
tv.alpha = clamped.coerceAtMost(1f)
|
||||
tv.scaleX = 0.9f + 0.1f * clamped.coerceAtMost(1f)
|
||||
tv.scaleY = tv.scaleX
|
||||
}
|
||||
|
||||
private fun fadeOut(tv: TextView) {
|
||||
tv.animate().alpha(0f).setDuration(150L).start()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.EdgeEffect
|
||||
import androidx.core.view.ViewCompat.TYPE_TOUCH
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.isEmpty
|
||||
@@ -23,6 +25,71 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
private val detachedViews = Collections.newSetFromMap(WeakHashMap<View, Boolean>())
|
||||
private var isFixingScroll = false
|
||||
|
||||
var isPullGestureEnabled: Boolean = false
|
||||
var pullThreshold: Float = 0.3f
|
||||
private var pullProgressTop: Float = 0f
|
||||
private var pullProgressBottom: Float = 0f
|
||||
private var pullListener: OnPullGestureListener? = null
|
||||
|
||||
init {
|
||||
setEdgeEffectFactory(object : EdgeEffectFactory() {
|
||||
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
|
||||
return object : EdgeEffect(view.context) {
|
||||
override fun onPull(deltaDistance: Float) {
|
||||
val sign = if (direction == DIRECTION_TOP) 1f else if (direction == DIRECTION_BOTTOM) 1f else 0f
|
||||
if (sign != 0f) onPull(deltaDistance, 0.5f)
|
||||
}
|
||||
|
||||
override fun onPull(deltaDistance: Float, displacement: Float) {
|
||||
if (!isPullGestureEnabled) return
|
||||
if (direction == DIRECTION_TOP) {
|
||||
pullProgressTop = (pullProgressTop + deltaDistance).coerceAtLeast(0f)
|
||||
pullListener?.onPullProgressTop(pullProgressTop / pullThreshold)
|
||||
} else if (direction == DIRECTION_BOTTOM) {
|
||||
pullProgressBottom = (pullProgressBottom + deltaDistance).coerceAtLeast(0f)
|
||||
pullListener?.onPullProgressBottom(pullProgressBottom / pullThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRelease() {
|
||||
if (!isPullGestureEnabled) {
|
||||
pullProgressTop = 0f
|
||||
pullProgressBottom = 0f
|
||||
return
|
||||
}
|
||||
var triggered = false
|
||||
if (direction == DIRECTION_TOP) {
|
||||
if (pullProgressTop >= pullThreshold) {
|
||||
pullListener?.onPullTriggeredTop()
|
||||
triggered = true
|
||||
}
|
||||
pullProgressTop = 0f
|
||||
pullListener?.onPullProgressTop(0f)
|
||||
} else if (direction == DIRECTION_BOTTOM) {
|
||||
if (pullProgressBottom >= pullThreshold) {
|
||||
pullListener?.onPullTriggeredBottom()
|
||||
triggered = true
|
||||
}
|
||||
pullProgressBottom = 0f
|
||||
pullListener?.onPullProgressBottom(0f)
|
||||
}
|
||||
if (!triggered) {
|
||||
pullListener?.onPullCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas?): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setOnPullGestureListener(listener: OnPullGestureListener?) {
|
||||
pullListener = listener
|
||||
}
|
||||
|
||||
override fun onChildDetachedFromWindow(child: View) {
|
||||
super.onChildDetachedFromWindow(child)
|
||||
detachedViews.add(child)
|
||||
@@ -188,4 +255,12 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
lastVisiblePosition: Int,
|
||||
)
|
||||
}
|
||||
|
||||
interface OnPullGestureListener {
|
||||
fun onPullProgressTop(progress: Float)
|
||||
fun onPullProgressBottom(progress: Float)
|
||||
fun onPullTriggeredTop()
|
||||
fun onPullTriggeredBottom()
|
||||
fun onPullCancelled()
|
||||
}
|
||||
}
|
||||
|
||||
18
app/src/main/res/drawable/ic_gesture.xml
Normal file
18
app/src/main/res/drawable/ic_gesture.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,2A10,10 0 1,0 22,12A10,10 0 0,0 12,2Z"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="@android:color/transparent"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,7l-3,3h2v4h2v-4h2z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,17l3,-3h-2v-4h-2v4h-2z"/>
|
||||
</vector>
|
||||
@@ -16,4 +16,30 @@
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />
|
||||
|
||||
<org.koitharu.kotatsu.reader.ui.ReaderToastView
|
||||
android:id="@+id/feedbackTop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:alpha="0"
|
||||
android:text="@string/pull_to_prev_chapter"
|
||||
android:textAppearance="?textAppearanceBodyLarge"
|
||||
android:background="@drawable/bg_reader_indicator"
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark" />
|
||||
|
||||
<org.koitharu.kotatsu.reader.ui.ReaderToastView
|
||||
android:id="@+id/feedbackBottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:alpha="0"
|
||||
android:text="@string/pull_to_next_chapter"
|
||||
android:textAppearance="?textAppearanceBodyLarge"
|
||||
android:background="@drawable/bg_reader_indicator"
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark" />
|
||||
|
||||
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame>
|
||||
|
||||
@@ -129,6 +129,20 @@
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
app:drawableStartCompat="@drawable/ic_split_horizontal" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switch_pull_gesture"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:minHeight="?android:listPreferredItemHeightSmall"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:text="@string/enable_pull_gesture_title"
|
||||
android:textAppearance="?textAppearanceListItem"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
app:drawableStartCompat="@drawable/ic_gesture" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
|
||||
android:id="@+id/button_screen_rotate"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -427,6 +427,10 @@
|
||||
<string name="data_and_privacy">Data and privacy</string>
|
||||
<string name="restore_summary">Restore previously created backup</string>
|
||||
<string name="webtoon_zoom_summary">Allow zoom in gesture in webtoon mode</string>
|
||||
<string name="pull_to_prev_chapter">Release to open previous chapter</string>
|
||||
<string name="pull_to_next_chapter">Release to open next chapter</string>
|
||||
<string name="pull_top_no_prev">No previous chapter</string>
|
||||
<string name="pull_bottom_no_next">No next chapter</string>
|
||||
<string name="reader_info_bar_summary">Show the current time and reading progress at the top of the screen</string>
|
||||
<string name="show_pages_numbers_summary">Show page numbers in bottom corner</string>
|
||||
<string name="clear_source_cookies_summary">Clear cookies for specified domain only. In most cases will invalidate authorization</string>
|
||||
@@ -644,6 +648,8 @@
|
||||
<string name="show_updated">Show updated</string>
|
||||
<string name="webtoon_gaps">Gaps in webtoon mode</string>
|
||||
<string name="webtoon_gaps_summary">Show vertical gaps between pages in webtoon mode</string>
|
||||
<string name="enable_pull_gesture_title">Enable pull gesture</string>
|
||||
<string name="enable_pull_gesture_summary">Use pull gesture to switch chapters in webtoon</string>
|
||||
<string name="less_frequently">Less frequently</string>
|
||||
<string name="more_frequently">More frequently</string>
|
||||
<string name="frequency_of_check">Frequency of check</string>
|
||||
|
||||
Reference in New Issue
Block a user