Skip to content

Commit eb6995c

Browse files
committed
refactor: migrate to viewpager2, improve fragment and state management
1 parent b2b26e1 commit eb6995c

File tree

10 files changed

+557
-285
lines changed

10 files changed

+557
-285
lines changed

readium/navigator/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dependencies {
3535
implementation(libs.androidx.legacy.ui)
3636
implementation(libs.androidx.lifecycle.common)
3737
implementation(libs.androidx.recyclerview)
38+
implementation(libs.androidx.viewpager2)
3839
implementation(libs.bundles.media3)
3940
implementation(libs.androidx.webkit)
4041

readium/navigator/src/main/java/org/readium/r2/navigator/R2WebView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ internal class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView
723723
val x = ev.safeGetX(pointerIndex)
724724
val xDiff = abs(x - mLastMotionX)
725725

726-
if (xDiff > mTouchSlop) {
726+
if (!scrollMode && xDiff > mTouchSlop) {
727727
if (DEBUG) Timber.v("Starting drag!")
728728
mIsBeingDragged = true
729729
mLastMotionX = if (x - mInitialMotionX > 0) {

readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import androidx.lifecycle.Lifecycle
3131
import androidx.lifecycle.lifecycleScope
3232
import androidx.lifecycle.repeatOnLifecycle
3333
import androidx.lifecycle.withStarted
34-
import androidx.viewpager.widget.ViewPager
34+
import androidx.viewpager2.widget.ViewPager2
3535
import kotlin.math.ceil
3636
import kotlin.reflect.KClass
3737
import kotlinx.coroutines.Job
@@ -425,7 +425,7 @@ public class EpubNavigatorFragment internal constructor(
425425
"The parent view of the EPUB `resourcePager` must be a ConstraintLayout"
426426
}
427427
// We need to null out the adapter explicitly, otherwise the page fragments will leak.
428-
resourcePager.adapter = null
428+
resourcePager.setAdapter(null)
429429
parent.removeView(resourcePager)
430430

431431
resourcePager = R2ViewPager(requireContext())
@@ -434,17 +434,96 @@ public class EpubNavigatorFragment internal constructor(
434434
EpubLayout.REFLOWABLE, null -> R2ViewPager.PublicationType.EPUB
435435
EpubLayout.FIXED -> R2ViewPager.PublicationType.FXL
436436
}
437+
438+
// Configure ViewPager orientation based on scroll settings
439+
if (viewModel.layout == EpubLayout.REFLOWABLE && viewModel.settings.value.scroll) {
440+
resourcePager.configureForVerticalScrolling()
441+
} else {
442+
resourcePager.configureForHorizontalPaging()
443+
}
444+
437445
resourcePager.setBackgroundColor(viewModel.settings.value.effectiveBackgroundColor)
438446
// Let the page views handle the keyboard events.
439447
resourcePager.isFocusable = false
440-
resourcePager.addOnPageChangeListener(PageChangeListener())
448+
resourcePager.registerOnPageChangeCallback(PageChangeListener())
449+
450+
// Set proper ConstraintLayout parameters
451+
val layoutParams = ConstraintLayout.LayoutParams(
452+
ConstraintLayout.LayoutParams.MATCH_CONSTRAINT,
453+
ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
454+
).apply {
455+
topToTop = ConstraintLayout.LayoutParams.PARENT_ID
456+
bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
457+
startToStart = ConstraintLayout.LayoutParams.PARENT_ID
458+
endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
459+
}
460+
resourcePager.layoutParams = layoutParams
441461

442462
parent.addView(resourcePager)
443463

444464
resetResourcePagerAdapter()
445465
}
446466

447-
private inner class PageChangeListener : ViewPager.SimpleOnPageChangeListener() {
467+
private inner class PageChangeListener : ViewPager2.OnPageChangeCallback() {
468+
private var hasPresetScrollPosition = false
469+
470+
override fun onPageScrollStateChanged(state: Int) {
471+
when (state) {
472+
ViewPager2.SCROLL_STATE_IDLE -> {
473+
// Reset flag when swipe ends
474+
hasPresetScrollPosition = false
475+
}
476+
else -> Unit
477+
}
478+
}
479+
480+
override fun onPageScrolled(
481+
position: Int,
482+
positionOffset: Float,
483+
positionOffsetPixels: Int,
484+
) {
485+
// Only process when swipe has started and scroll position hasn't been set yet
486+
if (positionOffset > 0f && !hasPresetScrollPosition) {
487+
val currentPosition = resourcePager.currentItem
488+
489+
// When swiping to previous page (position is less than current)
490+
if (position < currentPosition) {
491+
// Find target page fragment and set to last page
492+
val targetFragment = fragmentAt(position) as? R2EpubPageFragment
493+
targetFragment?.webView?.let { webView ->
494+
if (viewModel.isScrollEnabled.value) {
495+
webView.scrollToEnd()
496+
} else if (webView.numPages > 1) {
497+
if (resourcePager.isRtl()) {
498+
webView.setCurrentItem(0, false)
499+
} else {
500+
webView.setCurrentItem(webView.numPages - 1, false)
501+
}
502+
}
503+
}
504+
hasPresetScrollPosition = true
505+
}
506+
// When swiping to next page (position + 1 is greater than current)
507+
else if (position + 1 > currentPosition) {
508+
// Find target page fragment and set to first page
509+
val targetFragment = fragmentAt(position + 1) as? R2EpubPageFragment
510+
targetFragment?.webView?.let { webView ->
511+
if (viewModel.isScrollEnabled.value) {
512+
webView.scrollToStart()
513+
} else if (webView.numPages > 1) {
514+
if (resourcePager.isRtl()) {
515+
webView.setCurrentItem(webView.numPages - 1, false)
516+
} else {
517+
webView.setCurrentItem(0, false)
518+
}
519+
}
520+
}
521+
522+
hasPresetScrollPosition = true
523+
}
524+
}
525+
}
526+
448527
override fun onPageSelected(position: Int) {
449528
currentReflowablePageFragment?.webView?.let { webView ->
450529
if (viewModel.isScrollEnabled.value) {
@@ -456,16 +535,17 @@ public class EpubNavigatorFragment internal constructor(
456535
webView.scrollToEnd()
457536
}
458537
} else {
459-
if (currentPagerPosition < position) {
460-
// handle swipe LEFT
461-
webView.setCurrentItem(0, false)
462-
} else if (currentPagerPosition > position) {
463-
// handle swipe RIGHT
464-
webView.setCurrentItem(webView.numPages - 1, false)
538+
if (!hasPresetScrollPosition) {
539+
if (currentPagerPosition < position) {
540+
webView.setCurrentItem(0, false)
541+
} else if (currentPagerPosition > position) {
542+
webView.setCurrentItem(webView.numPages - 1, false)
543+
}
465544
}
466545
}
467546
}
468-
currentPagerPosition = position // Update current position
547+
548+
currentPagerPosition = position
469549

470550
notifyCurrentLocation()
471551
}
@@ -474,23 +554,23 @@ public class EpubNavigatorFragment internal constructor(
474554
private fun resetResourcePagerAdapter() {
475555
adapter = when (publication.metadata.presentation.layout) {
476556
EpubLayout.REFLOWABLE, null -> {
477-
R2PagerAdapter(childFragmentManager, resourcesSingle)
557+
R2PagerAdapter(this, resourcesSingle)
478558
}
479559
EpubLayout.FIXED -> {
480560
when (viewModel.dualPageMode) {
481561
// FIXME: Properly implement DualPage.AUTO depending on the device orientation.
482562
DualPage.OFF, DualPage.AUTO -> {
483-
R2PagerAdapter(childFragmentManager, resourcesSingle)
563+
R2PagerAdapter(this, resourcesSingle)
484564
}
485565
DualPage.ON -> {
486-
R2PagerAdapter(childFragmentManager, resourcesDouble)
566+
R2PagerAdapter(this, resourcesDouble)
487567
}
488568
}
489569
}
490570
}
491571
adapter.listener = PagerAdapterListener()
492-
resourcePager.adapter = adapter
493-
resourcePager.direction = overflow.value.readingProgression
572+
resourcePager.setAdapter(adapter)
573+
resourcePager.readingProgression = overflow.value.readingProgression
494574
resourcePager.layoutDirection = when (settings.value.readingProgression) {
495575
ReadingProgression.RTL -> LayoutDirection.RTL
496576
ReadingProgression.LTR -> LayoutDirection.LTR
@@ -625,11 +705,13 @@ public class EpubNavigatorFragment internal constructor(
625705
else -> false
626706
}
627707
} ?: return
708+
628709
val (index, _) = page
629710

630711
if (resourcePager.currentItem != index) {
631-
resourcePager.currentItem = index
712+
resourcePager.setCurrentItem(index, false)
632713
}
714+
633715
r2PagerAdapter?.loadLocatorAt(index, locator)
634716
}
635717

@@ -903,7 +985,7 @@ public class EpubNavigatorFragment internal constructor(
903985

904986
private fun goToNextResource(jump: Boolean, animated: Boolean): Boolean {
905987
val adapter = resourcePager.adapter ?: return false
906-
if (resourcePager.currentItem >= adapter.count - 1) {
988+
if (resourcePager.currentItem >= adapter.itemCount - 1) {
907989
return false
908990
}
909991

readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import android.view.View
1515
import android.view.ViewGroup
1616
import androidx.fragment.app.FragmentActivity
1717
import androidx.fragment.app.FragmentFactory
18-
import androidx.viewpager.widget.ViewPager
18+
import androidx.viewpager2.widget.ViewPager2
1919
import kotlinx.coroutines.flow.MutableStateFlow
2020
import kotlinx.coroutines.flow.StateFlow
2121
import kotlinx.coroutines.flow.asStateFlow
@@ -106,28 +106,28 @@ public class ImageNavigatorFragment private constructor(
106106

107107
positions = runBlocking { publication.positions() }
108108

109-
resourcePager.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
109+
resourcePager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
110110
override fun onPageSelected(position: Int) {
111111
notifyCurrentLocation()
112112
}
113113
})
114114

115115
val resources = publication.readingOrder
116116
.map { R2PagerAdapter.PageResource.Cbz(it) }
117-
adapter = R2PagerAdapter(childFragmentManager, resources)
117+
adapter = R2PagerAdapter(this, resources)
118118

119-
resourcePager.adapter = adapter
119+
resourcePager.setAdapter(adapter)
120120

121121
if (currentPagerPosition == 0) {
122122
if (requireActivity().layoutDirectionIsRTL()) {
123123
// The view has RTL layout
124-
resourcePager.currentItem = resources.size - 1
124+
resourcePager.setCurrentItem(resources.size - 1, false)
125125
} else {
126126
// The view has LTR layout
127-
resourcePager.currentItem = currentPagerPosition
127+
resourcePager.setCurrentItem(currentPagerPosition, false)
128128
}
129129
} else {
130-
resourcePager.currentItem = currentPagerPosition
130+
resourcePager.setCurrentItem(currentPagerPosition, false)
131131
}
132132

133133
if (initialLocator != null) {
@@ -175,7 +175,7 @@ public class ImageNavigatorFragment private constructor(
175175

176176
listener?.onJumpToLocator(locator)
177177
currentPagerPosition = resourceIndex
178-
resourcePager.currentItem = currentPagerPosition
178+
resourcePager.setCurrentItem(currentPagerPosition, false)
179179

180180
return true
181181
}
@@ -187,27 +187,15 @@ public class ImageNavigatorFragment private constructor(
187187

188188
override fun goForward(animated: Boolean): Boolean {
189189
val current = resourcePager.currentItem
190-
if (requireActivity().layoutDirectionIsRTL()) {
191-
// The view has RTL layout
192-
resourcePager.currentItem = current - 1
193-
} else {
194-
// The view has LTR layout
195-
resourcePager.currentItem = current + 1
196-
}
190+
resourcePager.setCurrentItem(current + 1, false)
197191

198192
notifyCurrentLocation()
199193
return current != resourcePager.currentItem
200194
}
201195

202196
override fun goBackward(animated: Boolean): Boolean {
203197
val current = resourcePager.currentItem
204-
if (requireActivity().layoutDirectionIsRTL()) {
205-
// The view has RTL layout
206-
resourcePager.currentItem = current + 1
207-
} else {
208-
// The view has LTR layout
209-
resourcePager.currentItem = current - 1
210-
}
198+
resourcePager.setCurrentItem(current - 1, false)
211199

212200
notifyCurrentLocation()
213201
return current != resourcePager.currentItem

readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2CbzPageFragment.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,13 @@ internal class R2CbzPageFragment(
8888

8989
private fun updatePadding() {
9090
viewLifecycleOwner.lifecycleScope.launch {
91-
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
91+
// Due to the migration from ViewPager to ViewPager2,
92+
// adjacent pages now transition to the RESUMED state at onPageSelected,
93+
// unlike the previous behavior.
94+
// Therefore, changing the lifecycle state from RESUMED to STARTED
95+
// allows padding to be pre-applied to the left and right pages,
96+
// ensuring consistent UI behavior during page transitions.
97+
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
9298
val window = activity?.window ?: return@repeatOnLifecycle
9399
var top = 0
94100
var bottom = 0

readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2EpubPageFragment.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,13 @@ internal class R2EpubPageFragment : Fragment() {
348348
if (view == null) return
349349

350350
viewLifecycleOwner.lifecycleScope.launch {
351-
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
351+
// Due to the migration from ViewPager to ViewPager2,
352+
// adjacent pages now transition to the RESUMED state at onPageSelected,
353+
// unlike the previous behavior.
354+
// Therefore, changing the lifecycle state from RESUMED to STARTED
355+
// allows padding to be pre-applied to the left and right pages,
356+
// ensuring consistent UI behavior during page transitions.
357+
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
352358
val window = activity?.window ?: return@repeatOnLifecycle
353359
var top = 0
354360
var bottom = 0
@@ -509,6 +515,7 @@ internal class R2EpubPageFragment : Fragment() {
509515
/**
510516
* Same as setOnClickListener, but will also report the tap point in the view.
511517
*/
518+
@SuppressLint("ClickableViewAccessibility")
512519
private fun View.setOnClickListenerWithPoint(action: (View, PointF) -> Unit) {
513520
var point = PointF()
514521

0 commit comments

Comments
 (0)