Merge pull request #1586 from MuhamadSyabitHidayattulloh/feat/pull-gesture-navigate-chapter
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)
|
get() = prefs.getBoolean(KEY_WEBTOON_GAPS, false)
|
||||||
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_GAPS, value) }
|
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)
|
@get:FloatRange(from = 0.0, to = 0.5)
|
||||||
val defaultWebtoonZoomOut: Float
|
val defaultWebtoonZoomOut: Float
|
||||||
get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f
|
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_GAPS = "webtoon_gaps"
|
||||||
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
||||||
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
|
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_PREFETCH_CONTENT = "prefetch_content"
|
||||||
const val KEY_APP_LOCALE = "app_locale"
|
const val KEY_APP_LOCALE = "app_locale"
|
||||||
const val KEY_SOURCES_GRID = "sources_grid"
|
const val KEY_SOURCES_GRID = "sources_grid"
|
||||||
|
|||||||
@@ -374,6 +374,9 @@ class ReaderActivity :
|
|||||||
viewBinding.infoBar.isTimeVisible = isFullscreen
|
viewBinding.infoBar.isTimeVisible = isFullscreen
|
||||||
updateScrollTimerButton()
|
updateScrollTimerButton()
|
||||||
systemUiController.setSystemUiVisible(isUiVisible || !isFullscreen)
|
systemUiController.setSystemUiVisible(isUiVisible || !isFullscreen)
|
||||||
|
val topOffset = if (isUiVisible) viewBinding.appbarTop.height else 0
|
||||||
|
val bottomOffset = if (isUiVisible) (viewBinding.toolbarDocked?.height ?: 0) else 0
|
||||||
|
viewModel.setReaderUiOffsets(topOffset, bottomOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,6 +398,10 @@ class ReaderActivity :
|
|||||||
viewBinding.infoBar.updatePadding(
|
viewBinding.infoBar.updatePadding(
|
||||||
top = systemBars.top,
|
top = systemBars.top,
|
||||||
)
|
)
|
||||||
|
viewModel.setReaderUiOffsets(
|
||||||
|
(if (viewBinding.appbarTop.isVisible) viewBinding.appbarTop.height else 0) + systemBars.top,
|
||||||
|
(if (viewBinding.toolbarDocked?.isVisible == true) (viewBinding.toolbarDocked?.height ?: 0) else 0) + systemBars.bottom,
|
||||||
|
)
|
||||||
return WindowInsetsCompat.Builder(insets)
|
return WindowInsetsCompat.Builder(insets)
|
||||||
.setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)
|
.setInsets(WindowInsetsCompat.Type.systemBars(), Insets.NONE)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ class ReaderViewModel @Inject constructor(
|
|||||||
val onAskNsfwIncognito = MutableEventFlow<Unit>()
|
val onAskNsfwIncognito = MutableEventFlow<Unit>()
|
||||||
val uiState = MutableStateFlow<ReaderUiState?>(null)
|
val uiState = MutableStateFlow<ReaderUiState?>(null)
|
||||||
|
|
||||||
|
val readerUiTopOffset = MutableStateFlow(0)
|
||||||
|
val readerUiBottomOffset = MutableStateFlow(0)
|
||||||
|
|
||||||
val isIncognitoMode = MutableStateFlow(savedStateHandle.get<Boolean>(ReaderIntent.EXTRA_INCOGNITO))
|
val isIncognitoMode = MutableStateFlow(savedStateHandle.get<Boolean>(ReaderIntent.EXTRA_INCOGNITO))
|
||||||
|
|
||||||
val content = MutableStateFlow(ReaderContent(emptyList(), null))
|
val content = MutableStateFlow(ReaderContent(emptyList(), null))
|
||||||
@@ -157,6 +160,12 @@ class ReaderViewModel @Inject constructor(
|
|||||||
valueProducer = { isWebtoonGapsEnabled },
|
valueProducer = { isWebtoonGapsEnabled },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val isWebtoonPullGestureEnabled = settings.observeAsStateFlow(
|
||||||
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
|
key = AppSettings.KEY_WEBTOON_PULL_GESTURE,
|
||||||
|
valueProducer = { isWebtoonPullGestureEnabled },
|
||||||
|
)
|
||||||
|
|
||||||
val defaultWebtoonZoomOut = observeIsWebtoonZoomEnabled().flatMapLatest {
|
val defaultWebtoonZoomOut = observeIsWebtoonZoomEnabled().flatMapLatest {
|
||||||
if (it) {
|
if (it) {
|
||||||
observeWebtoonZoomOut()
|
observeWebtoonZoomOut()
|
||||||
@@ -221,6 +230,11 @@ class ReaderViewModel @Inject constructor(
|
|||||||
discordRpc.setIdle()
|
discordRpc.setIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setReaderUiOffsets(top: Int, bottom: Int) {
|
||||||
|
readerUiTopOffset.value = top
|
||||||
|
readerUiBottomOffset.value = bottom
|
||||||
|
}
|
||||||
|
|
||||||
fun switchMode(newMode: ReaderMode) {
|
fun switchMode(newMode: ReaderMode) {
|
||||||
launchJob {
|
launchJob {
|
||||||
val manga = checkNotNull(getMangaOrNull())
|
val manga = checkNotNull(getMangaOrNull())
|
||||||
@@ -345,11 +359,14 @@ class ReaderViewModel @Inject constructor(
|
|||||||
return@launchJob
|
return@launchJob
|
||||||
}
|
}
|
||||||
ensureActive()
|
ensureActive()
|
||||||
if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
val autoLoadAllowed = readerMode.value != ReaderMode.WEBTOON || !isWebtoonPullGestureEnabled.value
|
||||||
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
if (autoLoadAllowed) {
|
||||||
}
|
if (upperPos >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
||||||
if (lowerPos <= BOUNDS_PAGE_OFFSET) {
|
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
||||||
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
}
|
||||||
|
if (lowerPos <= BOUNDS_PAGE_OFFSET) {
|
||||||
|
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (pageLoader.isPrefetchApplicable()) {
|
if (pageLoader.isPrefetchApplicable()) {
|
||||||
pageLoader.prefetch(pages.trySublist(upperPos + 1, upperPos + PREFETCH_LIMIT))
|
pageLoader.prefetch(pages.trySublist(upperPos + 1, upperPos + PREFETCH_LIMIT))
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ class ReaderConfigSheet :
|
|||||||
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
|
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
|
||||||
binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape
|
binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape
|
||||||
binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD || mode == ReaderMode.REVERSED
|
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.checkableGroup.addOnButtonCheckedListener(this)
|
||||||
binding.buttonSavePage.setOnClickListener(this)
|
binding.buttonSavePage.setOnClickListener(this)
|
||||||
@@ -96,6 +98,7 @@ class ReaderConfigSheet :
|
|||||||
binding.buttonScrollTimer.setOnClickListener(this)
|
binding.buttonScrollTimer.setOnClickListener(this)
|
||||||
binding.buttonBookmark.setOnClickListener(this)
|
binding.buttonBookmark.setOnClickListener(this)
|
||||||
binding.switchDoubleReader.setOnCheckedChangeListener(this)
|
binding.switchDoubleReader.setOnCheckedChangeListener(this)
|
||||||
|
binding.switchPullGesture.setOnCheckedChangeListener(this)
|
||||||
|
|
||||||
viewModel.isBookmarkAdded.observe(viewLifecycleOwner) {
|
viewModel.isBookmarkAdded.observe(viewLifecycleOwner) {
|
||||||
binding.buttonBookmark.setText(if (it) R.string.bookmark_remove else R.string.bookmark_add)
|
binding.buttonBookmark.setText(if (it) R.string.bookmark_remove else R.string.bookmark_add)
|
||||||
@@ -172,6 +175,10 @@ class ReaderConfigSheet :
|
|||||||
settings.isReaderDoubleOnLandscape = isChecked
|
settings.isReaderDoubleOnLandscape = isChecked
|
||||||
findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked)
|
findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.switch_pull_gesture -> {
|
||||||
|
settings.isWebtoonPullGestureEnabled = isChecked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +198,7 @@ class ReaderConfigSheet :
|
|||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
viewBinding?.switchDoubleReader?.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED
|
viewBinding?.switchDoubleReader?.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED
|
||||||
|
viewBinding?.switchPullGesture?.isEnabled = newMode == ReaderMode.WEBTOON
|
||||||
if (newMode == mode) {
|
if (newMode == mode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -46,6 +47,14 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
|||||||
|
|
||||||
override fun onViewBindingCreated(binding: FragmentReaderWebtoonBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: FragmentReaderWebtoonBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
|
var canGoPrev = true
|
||||||
|
var canGoNext = true
|
||||||
|
viewModel.readerUiTopOffset.observe(viewLifecycleOwner) { top ->
|
||||||
|
binding.feedbackTop.translationY = top.toFloat()
|
||||||
|
}
|
||||||
|
viewModel.readerUiBottomOffset.observe(viewLifecycleOwner) { bottom ->
|
||||||
|
binding.feedbackBottom.translationY = -bottom.toFloat()
|
||||||
|
}
|
||||||
with(binding.recyclerView) {
|
with(binding.recyclerView) {
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
adapter = readerAdapter
|
adapter = readerAdapter
|
||||||
@@ -53,6 +62,40 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
|||||||
recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also {
|
recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also {
|
||||||
addOnScrollListener(it)
|
addOnScrollListener(it)
|
||||||
}
|
}
|
||||||
|
setOnPullGestureListener(object : WebtoonRecyclerView.OnPullGestureListener {
|
||||||
|
override fun onPullProgressTop(progress: Float) {
|
||||||
|
if (canGoPrev) {
|
||||||
|
setFeedbackText(binding.feedbackTop, getString(R.string.pull_to_prev_chapter))
|
||||||
|
} else {
|
||||||
|
setFeedbackText(binding.feedbackTop, getString(R.string.pull_top_no_prev))
|
||||||
|
}
|
||||||
|
updateFeedback(binding.feedbackTop, progress)
|
||||||
|
}
|
||||||
|
override fun onPullProgressBottom(progress: Float) {
|
||||||
|
if (canGoNext) {
|
||||||
|
setFeedbackText(binding.feedbackBottom, getString(R.string.pull_to_next_chapter))
|
||||||
|
} else {
|
||||||
|
setFeedbackText(binding.feedbackBottom, getString(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) {
|
viewModel.isWebtoonZooEnabled.observe(viewLifecycleOwner) {
|
||||||
binding.frame.isZoomEnable = it
|
binding.frame.isZoomEnable = it
|
||||||
@@ -70,6 +113,21 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
|||||||
viewModel.readerSettingsProducer.observe(viewLifecycleOwner) {
|
viewModel.readerSettingsProducer.observe(viewLifecycleOwner) {
|
||||||
it.applyBackground(binding.root)
|
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() {
|
override fun onDestroyView() {
|
||||||
@@ -178,3 +236,24 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
|||||||
return getChildAdapterPosition(view)
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setFeedbackText(tv: TextView, text: CharSequence) {
|
||||||
|
if (tv.alpha <= 0f && text.isNotEmpty()) {
|
||||||
|
tv.alpha = 0f
|
||||||
|
tv.text = text
|
||||||
|
tv.animate().alpha(1f).setDuration(120L).start()
|
||||||
|
} else {
|
||||||
|
tv.text = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.EdgeEffect
|
||||||
import androidx.core.view.ViewCompat.TYPE_TOUCH
|
import androidx.core.view.ViewCompat.TYPE_TOUCH
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
import androidx.core.view.isEmpty
|
import androidx.core.view.isEmpty
|
||||||
@@ -23,6 +25,71 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
private val detachedViews = Collections.newSetFromMap(WeakHashMap<View, Boolean>())
|
private val detachedViews = Collections.newSetFromMap(WeakHashMap<View, Boolean>())
|
||||||
private var isFixingScroll = false
|
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) {
|
override fun onChildDetachedFromWindow(child: View) {
|
||||||
super.onChildDetachedFromWindow(child)
|
super.onChildDetachedFromWindow(child)
|
||||||
detachedViews.add(child)
|
detachedViews.add(child)
|
||||||
@@ -188,4 +255,12 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
|||||||
lastVisiblePosition: Int,
|
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"
|
android:orientation="vertical"
|
||||||
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />
|
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>
|
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame>
|
||||||
|
|||||||
@@ -129,6 +129,20 @@
|
|||||||
android:textColor="?colorOnSurfaceVariant"
|
android:textColor="?colorOnSurfaceVariant"
|
||||||
app:drawableStartCompat="@drawable/ic_split_horizontal" />
|
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
|
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
|
||||||
android:id="@+id/button_screen_rotate"
|
android:id="@+id/button_screen_rotate"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -427,6 +427,10 @@
|
|||||||
<string name="data_and_privacy">Data and privacy</string>
|
<string name="data_and_privacy">Data and privacy</string>
|
||||||
<string name="restore_summary">Restore previously created backup</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="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="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="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>
|
<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="show_updated">Show updated</string>
|
||||||
<string name="webtoon_gaps">Gaps in webtoon mode</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="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="less_frequently">Less frequently</string>
|
||||||
<string name="more_frequently">More frequently</string>
|
<string name="more_frequently">More frequently</string>
|
||||||
<string name="frequency_of_check">Frequency of check</string>
|
<string name="frequency_of_check">Frequency of check</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user