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
158 changes: 115 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,146 @@ 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) {
val vp = view.getChildAt(0) as? ViewPager2
val recyclerView = vp?.getChildAt(0) as? RecyclerView

// Stop any in-progress scroll/fling
recyclerView?.stopScroll()

// Disable item animations to prevent animator callbacks during teardown
recyclerView?.itemAnimator = null

// Clear cached views in the recycled pool
recyclerView?.recycledViewPool?.clear()

try {
// Clear adapter to prevent post-teardown fling callbacks.
// setAdapter(null) internally calls removeAndRecycleAllViews which
// can throw if views are still attached during mid-scroll teardown.
recyclerView?.adapter = null
} catch (_: IllegalArgumentException) {
// Safe to ignore during teardown — view is being destroyed
}
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 +243,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 +267,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)
Comment thread
MrRefactor marked this conversation as resolved.

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