Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 168 additions & 43 deletions android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package com.reactnativepagerview

import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.facebook.infer.annotation.Assertions
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
import com.facebook.react.module.annotations.ReactModule
Expand All @@ -18,9 +18,10 @@ import com.reactnativepagerview.event.PageScrollEvent
import com.reactnativepagerview.event.PageScrollStateChangedEvent
import com.reactnativepagerview.event.PageSelectedEvent


@ReactModule(name = PagerViewViewManagerImpl.NAME)
class PagerViewViewManager : ViewGroupManager<NestedScrollableHost>(), RNCViewPagerManagerInterface<NestedScrollableHost> {
class PagerViewViewManager :
ViewGroupManager<NestedScrollableHost>(),
RNCViewPagerManagerInterface<NestedScrollableHost> {
companion object {
init {
if (BuildConfig.CODEGEN_MODULE_REGISTRATION != null) {
Expand All @@ -29,84 +30,199 @@ class PagerViewViewManager : ViewGroupManager<NestedScrollableHost>(), RNCViewPa
}
}

private val mDelegate: ViewManagerDelegate<NestedScrollableHost> = RNCViewPagerManagerDelegate(this)
private val mDelegate: ViewManagerDelegate<NestedScrollableHost> =
RNCViewPagerManagerDelegate(this)

override fun getDelegate() = mDelegate

override fun getName(): String {
return PagerViewViewManagerImpl.NAME
}

override fun receiveCommand(root: NestedScrollableHost, commandId: String, args: ReadableArray?) {
override fun receiveCommand(
root: NestedScrollableHost,
commandId: String,
args: ReadableArray?
) {
mDelegate.receiveCommand(root, commandId, args)
}

public override fun createViewInstance(reactContext: ThemedReactContext): NestedScrollableHost {
val host = NestedScrollableHost(reactContext)
host.id = View.generateViewId()
host.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
host.layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
host.isSaveEnabled = false
val vp = ViewPager2(reactContext)

// Access private mRecyclerView field using reflection to disable animations
val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
recyclerViewField.isAccessible = true
val recyclerView = recyclerViewField.get(vp) as RecyclerView

// Disable all animations to prevent layout change issues
recyclerView.itemAnimator = null
recyclerView.layoutTransition = null

vp.adapter = ViewPagerAdapter()
//https://github.com/callstack/react-native-viewpager/issues/183
// https://github.com/callstack/react-native-viewpager/issues/183
vp.isSaveEnabled = false

vp.post {
vp.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)?.dispatchEvent(
PageScrollEvent(host.id, position, positionOffset)
)
}

override fun onPageSelected(position: Int) {
super.onPageSelected(position)
UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)?.dispatchEvent(
PageSelectedEvent(host.id, position)
)
}

override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
val pageScrollState: String = when (state) {
ViewPager2.SCROLL_STATE_IDLE -> "idle"
ViewPager2.SCROLL_STATE_DRAGGING -> "dragging"
ViewPager2.SCROLL_STATE_SETTLING -> "settling"
else -> throw IllegalStateException("Unsupported pageScrollState")
vp.registerOnPageChangeCallback(
object : OnPageChangeCallback() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)
?.dispatchEvent(
PageScrollEvent(host.id, position, positionOffset)
)
}

override fun onPageSelected(position: Int) {
super.onPageSelected(position)
UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)
?.dispatchEvent(PageSelectedEvent(host.id, position))
}

override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
val pageScrollState: String =
when (state) {
ViewPager2.SCROLL_STATE_IDLE -> "idle"
ViewPager2.SCROLL_STATE_DRAGGING -> "dragging"
ViewPager2.SCROLL_STATE_SETTLING -> "settling"
else ->
throw IllegalStateException(
"Unsupported pageScrollState"
)
}
UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)
?.dispatchEvent(
PageScrollStateChangedEvent(host.id, pageScrollState)
)
}
}
UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)?.dispatchEvent(
PageScrollStateChangedEvent(host.id, pageScrollState)
)
}
})
UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)?.dispatchEvent(
PageSelectedEvent(host.id, vp.currentItem)
)
UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)
?.dispatchEvent(PageSelectedEvent(host.id, vp.currentItem))
}
host.addView(vp)
return host
}

private fun stopScrollIfNeeded(host: NestedScrollableHost) {
val recyclerView = (host.getChildAt(0) as? ViewPager2)?.getChildAt(0) as? RecyclerView
recyclerView?.stopScroll()
}

override fun onDropViewInstance(view: NestedScrollableHost) {
try {
val viewPager = PagerViewViewManagerImpl.getViewPager(view)
// Access private mRecyclerView field using reflection
val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
recyclerViewField.isAccessible = true
val recyclerView = recyclerViewField.get(viewPager) as RecyclerView

// Stop any scroll/drag in progress first
recyclerView.stopScroll()

// Suppress layout to prevent any pending layout operations from executing
recyclerView.suppressLayout(true)

// Access and stop the ViewFlinger directly via reflection - do this early
try {
val viewFlingerField = RecyclerView::class.java.getDeclaredField("mViewFlinger")
viewFlingerField.isAccessible = true
val viewFlinger = viewFlingerField.get(recyclerView) as? Runnable
viewFlinger?.let {
recyclerView.removeCallbacks(it)
// Also try to stop it via reflection if it has a stop method
try {
val stopMethod = viewFlinger.javaClass.getDeclaredMethod("stop")
stopMethod.isAccessible = true
stopMethod.invoke(viewFlinger)
} catch (ignored: Exception) {}
}
} catch (ignored: Exception) {
// ViewFlinger reflection may fail on some versions
}

// Clear the recycler's scrap views before clearing adapter
try {
val recyclerField = RecyclerView::class.java.getDeclaredField("mRecycler")
recyclerField.isAccessible = true
val recycler = recyclerField.get(recyclerView)

// Clear scrap heaps
val clearScrapMethod = recycler.javaClass.getDeclaredMethod("clear")
clearScrapMethod.isAccessible = true
clearScrapMethod.invoke(recycler)
} catch (ignored: Exception) {
// Recycler reflection may fail on some versions
}

// Clear any pending animations and layout transitions
recyclerView.clearAnimation()
recyclerView.itemAnimator = null
recyclerView.layoutTransition = null
recyclerView.recycledViewPool.clear()

// Remove all pending operations and choreographer callbacks
recyclerView.removeCallbacks(null)

// Try to clear any pending adapter updates
try {
val adapterHelperField = RecyclerView::class.java.getDeclaredField("mAdapterHelper")
adapterHelperField.isAccessible = true
val adapterHelper = adapterHelperField.get(recyclerView)
val resetMethod = adapterHelper.javaClass.getDeclaredMethod("reset")
resetMethod.isAccessible = true
resetMethod.invoke(adapterHelper)
} catch (ignored: Exception) {}

// Clear the adapter to prevent recycling during teardown
viewPager.adapter = null

// Unsuppress layout after clearing adapter (won't do anything as view is being
// destroyed)
recyclerView.suppressLayout(false)
} catch (e: Exception) {
// View might already be in an invalid state
}
super.onDropViewInstance(view)
}

override fun addView(host: NestedScrollableHost, child: View, index: Int) {
PagerViewViewManagerImpl.addView(host, child, index)
}

override fun getChildCount(parent: NestedScrollableHost) = PagerViewViewManagerImpl.getChildCount(parent)
override fun getChildCount(parent: NestedScrollableHost) =
PagerViewViewManagerImpl.getChildCount(parent)

override fun getChildAt(parent: NestedScrollableHost, index: Int): View {
return PagerViewViewManagerImpl.getChildAt(parent, index)
}

override fun removeView(parent: NestedScrollableHost, view: View) {
stopScrollIfNeeded(parent)
PagerViewViewManagerImpl.removeView(parent, view)
}

override fun removeAllViews(parent: NestedScrollableHost) {
stopScrollIfNeeded(parent)
PagerViewViewManagerImpl.removeAllViews(parent)
}

override fun removeViewAt(parent: NestedScrollableHost, index: Int) {
stopScrollIfNeeded(parent)
PagerViewViewManagerImpl.removeViewAt(parent, index)
}

Expand Down Expand Up @@ -180,7 +296,11 @@ class PagerViewViewManager : ViewGroupManager<NestedScrollableHost>(), RNCViewPa
val view = PagerViewViewManagerImpl.getViewPager(root)
Assertions.assertNotNull(view)
val childCount = view.adapter?.itemCount
val canScroll = childCount != null && childCount > 0 && selectedPage >= 0 && selectedPage < childCount
val canScroll =
childCount != null &&
childCount > 0 &&
selectedPage >= 0 &&
selectedPage < childCount
if (canScroll) {
PagerViewViewManagerImpl.setCurrentItem(view, selectedPage, scrollWithAnimation)
}
Expand All @@ -200,10 +320,15 @@ class PagerViewViewManager : ViewGroupManager<NestedScrollableHost>(), RNCViewPa
}
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Map<String, String>> {
override fun getExportedCustomDirectEventTypeConstants():
MutableMap<String, Map<String, String>> {
return MapBuilder.of(
PageScrollEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPageScroll"),
PageScrollStateChangedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPageScrollStateChanged"),
PageSelectedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPageSelected"))
PageScrollEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onPageScroll"),
PageScrollStateChangedEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onPageScrollStateChanged"),
PageSelectedEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onPageSelected")
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,7 @@ object PagerViewViewManagerImpl {

fun removeViewAt(parent: NestedScrollableHost, index: Int) {
val pager = getViewPager(parent)
val adapter = pager.adapter as ViewPagerAdapter?

val child = adapter?.getChildAt(index)

if (child != null && child.parent != null) {
(child.parent as? ViewGroup)?.removeView(child)
}

adapter?.removeChildAt(index)

(pager.adapter as? ViewPagerAdapter)?.removeChildAt(index)
debouncedRefreshViewChildrenLayout(pager)
}

Expand Down
32 changes: 20 additions & 12 deletions android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import android.widget.FrameLayout
import androidx.recyclerview.widget.RecyclerView.Adapter
import java.util.*

private fun View.detachFromParent() {
(parent as? ViewGroup)?.removeView(this)
}

class ViewPagerAdapter() : Adapter<ViewPagerViewHolder>() {
private val childrenViews: ArrayList<View> = ArrayList()
Expand All @@ -17,19 +20,26 @@ class ViewPagerAdapter() : Adapter<ViewPagerViewHolder>() {
override fun onBindViewHolder(holder: ViewPagerViewHolder, index: Int) {
val container: FrameLayout = holder.container
val child = getChildAt(index)
holder.setIsRecyclable(false)

if (container.childCount > 0) {
container.removeAllViews()
}

if (child.parent != null) {
(child.parent as FrameLayout).removeView(child)
}
child.detachFromParent()

container.addView(child)
}

override fun onViewRecycled(holder: ViewPagerViewHolder) {
super.onViewRecycled(holder)
holder.container.removeAllViews()
}

override fun onFailedToRecycleView(holder: ViewPagerViewHolder): Boolean {
holder.container.removeAllViews()
return true
}

override fun getItemCount(): Int {
return childrenViews.size
}
Expand All @@ -45,26 +55,24 @@ class ViewPagerAdapter() : Adapter<ViewPagerViewHolder>() {

fun removeChild(child: View) {
val index = childrenViews.indexOf(child)
if(index > -1) {

if (index > -1) {
removeChildAt(index)
}
}

fun removeAll() {
for (index in 1..childrenViews.size) {
val child = childrenViews[index-1]
if (child.parent?.parent != null) {
(child.parent.parent as ViewGroup).removeView(child.parent as View)
}
for (child in childrenViews) {
child.detachFromParent()
}
val removedChildrenCount = childrenViews.size
childrenViews.clear()
notifyItemRangeRemoved(0, removedChildrenCount)
}

fun removeChildAt(index: Int) {
if (index >= 0 && index < childrenViews.size) {
if (index >= 0 && index < childrenViews.size) {
childrenViews[index].detachFromParent()
childrenViews.removeAt(index)
notifyItemRemoved(index)
}
Expand Down
Loading
Loading