@onekeyfe/react-native-pager-view 6.7.0-alpha.383 → 6.7.0-alpha.385

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,8 @@ import android.view.View
12
12
  import android.view.ViewGroup
13
13
  import android.widget.HorizontalScrollView
14
14
  import android.widget.TextView
15
+ import androidx.recyclerview.widget.RecyclerView
16
+ import androidx.viewpager2.widget.ViewPager2
15
17
  import com.facebook.react.uimanager.PointerEvents
16
18
  import com.facebook.react.views.view.ReactViewGroup
17
19
  import java.util.concurrent.atomic.AtomicInteger
@@ -244,4 +246,61 @@ class CollapsiblePagerTabReservationTest : InstrumentationTestCase() {
244
246
  instrumentation.waitForIdleSync()
245
247
  }
246
248
  }
249
+
250
+ fun testUnmountWithTransitioningPageHoldersSkipsEmptyRecyclerLayout() {
251
+ val context = instrumentation.targetContext
252
+ val activity = instrumentation.startActivitySync(
253
+ Intent(context, CollapsiblePagerHitTestActivity::class.java)
254
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
255
+ ) as CollapsiblePagerHitTestActivity
256
+ try {
257
+ instrumentation.runOnMainSync {
258
+ val density = context.resources.displayMetrics.density
259
+ fun dp(value: Int) = (value * density).roundToInt()
260
+ val width = dp(320)
261
+ val height = dp(500)
262
+ val host = CollapsiblePagerHost(context)
263
+ activity.setContentView(host)
264
+ val header = View(context)
265
+ val sticky = View(context)
266
+ val page0 = View(context)
267
+ val page1 = View(context)
268
+ host.addReactChild(header, 0)
269
+ host.addReactChild(sticky, 1)
270
+ host.addReactChild(page0, 2)
271
+ host.addReactChild(page1, 3)
272
+ host.measure(
273
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
274
+ View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
275
+ )
276
+ host.layout(0, 0, width, height)
277
+ val pager = (0 until host.childCount).map(host::getChildAt).filterIsInstance<ViewPager2>().first()
278
+ val recycler = pager.getChildAt(0) as RecyclerView
279
+ assertNull(recycler.itemAnimator)
280
+ assertTrue(host.attachedPageCount() > 0)
281
+
282
+ val transitioningHolders = (0 until recycler.childCount).map(recycler::getChildAt)
283
+ transitioningHolders.forEach { recycler.startViewTransition(it) }
284
+
285
+ host.removeReactChild(page1)
286
+ host.removeReactChild(page0)
287
+ host.removeReactChild(sticky)
288
+ host.removeReactChild(header)
289
+
290
+ host.viewTreeObserver.dispatchOnPreDraw()
291
+ host.measure(
292
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
293
+ View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
294
+ )
295
+ host.layout(0, 0, width, height)
296
+ assertTrue(host.attachedPageCount() > 0)
297
+
298
+ transitioningHolders.forEach { recycler.endViewTransition(it) }
299
+ }
300
+ instrumentation.waitForIdleSync()
301
+ } finally {
302
+ instrumentation.runOnMainSync { activity.finish() }
303
+ instrumentation.waitForIdleSync()
304
+ }
305
+ }
247
306
  }
@@ -327,6 +327,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
327
327
  pager.id = View.generateViewId()
328
328
  pager.adapter = adapter
329
329
  pager.isSaveEnabled = false
330
+ (pager.getChildAt(0) as? RecyclerView)?.itemAnimator = null
330
331
  super.addView(
331
332
  pager,
332
333
  LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
@@ -708,7 +709,16 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
708
709
 
709
710
  private fun layoutPagerIfRequested() {
710
711
  val recycler = pager.getChildAt(0) as? RecyclerView ?: return
711
- if (!recycler.isLayoutRequested || recycler.isLayoutSuppressed || recycler.isComputingLayout || recycler.width <= 0 || recycler.height <= 0) return
712
+ if (
713
+ !shouldDrainCollapsiblePagerLayout(
714
+ recycler.isLayoutRequested,
715
+ recycler.isLayoutSuppressed,
716
+ recycler.isComputingLayout,
717
+ recycler.width,
718
+ recycler.height,
719
+ adapter.itemCount,
720
+ )
721
+ ) return
712
722
  // OneKey patch: a non-animated page command queues RecyclerView layout, but RN
713
723
  // can stop that request after navigation. Drain it before showing the page.
714
724
  recycler.measure(
@@ -732,7 +742,11 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
732
742
  }
733
743
 
734
744
  private fun isPagerRecyclerLayoutSuppressed(): Boolean =
735
- (pager.getChildAt(0) as? RecyclerView)?.isLayoutSuppressed == true
745
+ shouldSuppressCollapsiblePagerRecyclerLayout(
746
+ (pager.getChildAt(0) as? RecyclerView)?.isLayoutSuppressed == true,
747
+ adapter.itemCount,
748
+ attachedPageCount(),
749
+ )
736
750
 
737
751
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
738
752
  val width = MeasureSpec.getSize(widthMeasureSpec)
@@ -39,3 +39,22 @@ internal class NativeRefreshForegroundAlpha(initialAlpha: Float) {
39
39
  return lastApplied
40
40
  }
41
41
  }
42
+
43
+ // Skip ViewPager2 RecyclerView layout when explicitly suppressed or when all React page
44
+ // children have been removed on unmount while rendered ViewHolder views remain attached.
45
+ internal fun shouldSuppressCollapsiblePagerRecyclerLayout(
46
+ isLayoutSuppressed: Boolean,
47
+ adapterItemCount: Int,
48
+ attachedPageCount: Int,
49
+ ): Boolean = isLayoutSuppressed || (adapterItemCount <= 0 && attachedPageCount > 0)
50
+
51
+ internal fun shouldDrainCollapsiblePagerLayout(
52
+ isLayoutRequested: Boolean,
53
+ isLayoutSuppressed: Boolean,
54
+ isComputingLayout: Boolean,
55
+ width: Int,
56
+ height: Int,
57
+ adapterItemCount: Int,
58
+ ): Boolean = adapterItemCount > 0 && isLayoutRequested && !isLayoutSuppressed &&
59
+ !isComputingLayout && width > 0 && height > 0
60
+
@@ -72,4 +72,44 @@ fun main() {
72
72
  check(nativeRefreshForegroundTop(37 + 230 + 64, 294f) == 37f)
73
73
  check(nativeRefreshForegroundTop(-40 + 294, 294f) == -40f)
74
74
  println("Refresh foreground policy: repeat, owner transfer/release, external alpha, transparent baseline, caller offset, and combined nested inset passed")
75
+
76
+ // Initial mount before any page ViewHolder is attached still measures ViewPager2.
77
+ check(!shouldSuppressCollapsiblePagerRecyclerLayout(isLayoutSuppressed = false, adapterItemCount = 0, attachedPageCount = 0))
78
+ // Unmount removes all adapter pages while rendered ViewHolder views remain attached.
79
+ check(shouldSuppressCollapsiblePagerRecyclerLayout(isLayoutSuppressed = false, adapterItemCount = 0, attachedPageCount = 1))
80
+ check(shouldSuppressCollapsiblePagerRecyclerLayout(isLayoutSuppressed = true, adapterItemCount = 2, attachedPageCount = 1))
81
+ // Re-adding pages resumes ViewPager2 layout.
82
+ check(!shouldSuppressCollapsiblePagerRecyclerLayout(isLayoutSuppressed = false, adapterItemCount = 1, attachedPageCount = 1))
83
+
84
+ check(
85
+ !shouldDrainCollapsiblePagerLayout(
86
+ isLayoutRequested = true,
87
+ isLayoutSuppressed = false,
88
+ isComputingLayout = false,
89
+ width = 1080,
90
+ height = 2400,
91
+ adapterItemCount = 0,
92
+ ),
93
+ )
94
+ check(
95
+ shouldDrainCollapsiblePagerLayout(
96
+ isLayoutRequested = true,
97
+ isLayoutSuppressed = false,
98
+ isComputingLayout = false,
99
+ width = 1080,
100
+ height = 2400,
101
+ adapterItemCount = 2,
102
+ ),
103
+ )
104
+ check(
105
+ !shouldDrainCollapsiblePagerLayout(
106
+ isLayoutRequested = true,
107
+ isLayoutSuppressed = true,
108
+ isComputingLayout = false,
109
+ width = 1080,
110
+ height = 2400,
111
+ adapterItemCount = 2,
112
+ ),
113
+ )
114
+ println("CollapsiblePager unmount layout guard: initial mount, unmount retention, suppression, and re-add passed")
75
115
  }
package/docs/SPEC.md CHANGED
@@ -357,8 +357,19 @@ Status: implemented locally; unreleased. On the Android UI thread, during a
357
357
  screen removal transition,
358
358
  NativeList and CollapsiblePager layout entry points respect the RecyclerView
359
359
  suppression state established by the screen-transition owner. The owner restores
360
- the original state when its last transition ownership ends. This adds no public
361
- API and does not change Web or iOS behavior.
360
+ the original state when its last transition ownership ends. In addition,
361
+ `CollapsiblePagerHost` disables `DefaultItemAnimator` on its internal
362
+ `ViewPager2` `RecyclerView` (`itemAnimator = null`) and skips pre-draw
363
+ (`layoutPagerIfRequested`) and host-driven `ViewPager2` layout passes when all
364
+ React page children have been removed from the adapter (`adapter.itemCount == 0`)
365
+ while previously rendered page `ViewHolder` views remain attached
366
+ (`attachedPageCount() > 0`), matching `PagerViewViewManagerImpl`'s unmount
367
+ guard. Initial empty measurement before any page is attached
368
+ (`attachedPageCount() == 0`) and subsequent page insertions (`adapter.itemCount > 0`)
369
+ still measure and lay out normally. This prevents unmount-time empty-adapter
370
+ layout passes from clearing visible exit frames or recycling attached
371
+ `ViewHolder` views held in `mDisappearingChildren` by `ViewGroup.startViewTransition`.
372
+ This adds no public API and does not change Web or iOS behavior.
362
373
 
363
374
  If Fabric removes the current NativeScroller React child while its origin-frame
364
375
  transition remains active, Android retains the already displayed native extent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-pager-view",
3
- "version": "6.7.0-alpha.383",
3
+ "version": "6.7.0-alpha.385",
4
4
  "description": "React Native wrapper for Android and iOS ViewPager",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -73,7 +73,7 @@
73
73
  "typescript": "^5.9.2"
74
74
  },
75
75
  "peerDependencies": {
76
- "@onekeyfe/react-native-native-logger": "6.7.0-alpha.383",
76
+ "@onekeyfe/react-native-native-logger": "6.7.0-alpha.385",
77
77
  "react": "*",
78
78
  "react-native": "*"
79
79
  },