Webtoon pull gesture refactoring
This commit is contained in:
2
.idea/.gitignore
generated
vendored
2
.idea/.gitignore
generated
vendored
@@ -3,3 +3,5 @@
|
||||
/workspace.xml
|
||||
/migrations.xml
|
||||
/runConfigurations.xml
|
||||
/appInsightsSettings.xml
|
||||
/kotlinCodeInsightSettings.xml
|
||||
|
||||
@@ -28,7 +28,8 @@ import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>(),
|
||||
WebtoonRecyclerView.OnWebtoonScrollListener {
|
||||
WebtoonRecyclerView.OnWebtoonScrollListener,
|
||||
WebtoonRecyclerView.OnPullGestureListener {
|
||||
|
||||
@Inject
|
||||
lateinit var networkState: NetworkState
|
||||
@@ -39,6 +40,8 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
private val scrollInterpolator = DecelerateInterpolator()
|
||||
|
||||
private var recyclerLifecycleDispatcher: RecyclerViewLifecycleDispatcher? = null
|
||||
private var canGoPrev = true
|
||||
private var canGoNext = true
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
@@ -47,8 +50,6 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentReaderWebtoonBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
var canGoPrev = true
|
||||
var canGoNext = true
|
||||
viewModel.readerUiTopOffset.observe(viewLifecycleOwner) { top ->
|
||||
binding.feedbackTop.translationY = top.toFloat()
|
||||
}
|
||||
@@ -62,40 +63,7 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
recyclerLifecycleDispatcher = RecyclerViewLifecycleDispatcher().also {
|
||||
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)
|
||||
}
|
||||
})
|
||||
setOnPullGestureListener(this@WebtoonReaderFragment)
|
||||
}
|
||||
viewModel.isWebtoonZooEnabled.observe(viewLifecycleOwner) {
|
||||
binding.frame.isZoomEnable = it
|
||||
@@ -113,11 +81,8 @@ 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
|
||||
binding.recyclerView.isPullGestureEnabled = enabled
|
||||
}
|
||||
viewModel.uiState.observe(viewLifecycleOwner) { state ->
|
||||
if (state != null) {
|
||||
@@ -226,6 +191,47 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPullProgressTop(progress: Float) {
|
||||
val binding = viewBinding ?: return
|
||||
if (canGoPrev) {
|
||||
binding.feedbackTop.setFeedbackText(getString(R.string.pull_to_prev_chapter))
|
||||
} else {
|
||||
binding.feedbackTop.setFeedbackText(getString(R.string.pull_top_no_prev))
|
||||
}
|
||||
binding.feedbackTop.updateFeedback(progress)
|
||||
}
|
||||
|
||||
override fun onPullProgressBottom(progress: Float) {
|
||||
val binding = viewBinding ?: return
|
||||
if (canGoNext) {
|
||||
binding.feedbackBottom.setFeedbackText(getString(R.string.pull_to_next_chapter))
|
||||
} else {
|
||||
binding.feedbackBottom.setFeedbackText(getString(R.string.pull_bottom_no_next))
|
||||
}
|
||||
binding.feedbackBottom.updateFeedback(progress)
|
||||
}
|
||||
|
||||
override fun onPullTriggeredTop() {
|
||||
(viewBinding ?: return).feedbackTop.fadeOut()
|
||||
if (canGoPrev) {
|
||||
viewModel.switchChapterBy(-1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPullTriggeredBottom() {
|
||||
(viewBinding ?: return).feedbackBottom.fadeOut()
|
||||
if (canGoNext) {
|
||||
viewModel.switchChapterBy(1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPullCancelled() {
|
||||
viewBinding?.apply {
|
||||
feedbackTop.fadeOut()
|
||||
feedbackBottom.fadeOut()
|
||||
}
|
||||
}
|
||||
|
||||
private fun RecyclerView.findCurrentPagePosition(): Int {
|
||||
val centerX = width / 2f
|
||||
val centerY = height - resources.getDimension(R.dimen.webtoon_pages_gap)
|
||||
@@ -235,25 +241,25 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
val view = findChildViewUnder(centerX, centerY) ?: return RecyclerView.NO_POSITION
|
||||
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 TextView.updateFeedback(progress: Float) {
|
||||
val clamped = progress.coerceIn(0f, 1.2f)
|
||||
this.alpha = clamped.coerceAtMost(1f)
|
||||
this.scaleX = 0.9f + 0.1f * clamped.coerceAtMost(1f)
|
||||
this.scaleY = this.scaleX
|
||||
}
|
||||
|
||||
private fun fadeOut(tv: TextView) {
|
||||
tv.animate().alpha(0f).setDuration(150L).start()
|
||||
}
|
||||
private fun TextView.fadeOut() {
|
||||
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
|
||||
private fun TextView.setFeedbackText(text: CharSequence) {
|
||||
if (this.alpha <= 0f && text.isNotEmpty()) {
|
||||
this.alpha = 0f
|
||||
this.text = text
|
||||
animate().alpha(1f).setDuration(120L).start()
|
||||
} else {
|
||||
this.text = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import androidx.core.view.isNotEmpty
|
||||
import androidx.core.view.iterator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.DIRECTION_BOTTOM
|
||||
import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.DIRECTION_TOP
|
||||
import java.util.Collections
|
||||
import java.util.LinkedList
|
||||
import java.util.WeakHashMap
|
||||
@@ -27,63 +29,10 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
setEdgeEffectFactory(PullEffect.Factory())
|
||||
}
|
||||
|
||||
fun setOnPullGestureListener(listener: OnPullGestureListener?) {
|
||||
@@ -246,6 +195,68 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private class PullEffect(
|
||||
view: RecyclerView,
|
||||
private val direction: Int,
|
||||
private val pullThreshold: Float,
|
||||
private val pullListener: OnPullGestureListener,
|
||||
) : EdgeEffect(view.context) {
|
||||
|
||||
private var pullProgressTop: Float = 0f
|
||||
private var pullProgressBottom: Float = 0f
|
||||
|
||||
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 (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() {
|
||||
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 = false
|
||||
|
||||
class Factory : EdgeEffectFactory() {
|
||||
|
||||
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
|
||||
val pullListener = (view as? WebtoonRecyclerView)?.pullListener
|
||||
return if (pullListener != null && view.isPullGestureEnabled) {
|
||||
PullEffect(view, direction, view.pullThreshold, pullListener)
|
||||
} else {
|
||||
super.createEdgeEffect(view, direction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OnWebtoonScrollListener {
|
||||
|
||||
fun onScrollChanged(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -16,30 +17,36 @@
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />
|
||||
|
||||
<org.koitharu.kotatsu.reader.ui.ReaderToastView
|
||||
<TextView
|
||||
android:id="@+id/feedbackTop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:layout_gravity="top|center_horizontal"
|
||||
android:layout_margin="@dimen/screen_padding"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/bg_reader_indicator"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:paddingVertical="@dimen/margin_small"
|
||||
android:text="@string/pull_to_prev_chapter"
|
||||
android:textAppearance="?textAppearanceBodyLarge"
|
||||
android:background="@drawable/bg_reader_indicator"
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark" />
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark"
|
||||
tools:alpha="1" />
|
||||
|
||||
<org.koitharu.kotatsu.reader.ui.ReaderToastView
|
||||
<TextView
|
||||
android:id="@+id/feedbackBottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="center"
|
||||
android:padding="16dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_margin="@dimen/screen_padding"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/bg_reader_indicator"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="@dimen/margin_normal"
|
||||
android:paddingVertical="@dimen/margin_small"
|
||||
android:text="@string/pull_to_next_chapter"
|
||||
android:textAppearance="?textAppearanceBodyLarge"
|
||||
android:background="@drawable/bg_reader_indicator"
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark" />
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark"
|
||||
tools:alpha="1" />
|
||||
|
||||
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame>
|
||||
|
||||
Reference in New Issue
Block a user