@onekeyfe/react-native-pager-view 6.7.0-2 → 6.7.0-20
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.
- package/README.md +14 -0
- package/android/src/androidTest/AndroidManifest.xml +7 -0
- package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +306 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +90 -21
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +72 -7
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +27 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +19 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +50 -6
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +40 -0
- package/docs/SPEC.md +253 -10
- package/ios/RNCCollapsiblePagerViewComponentView.mm +614 -42
- package/ios/RNCNativeScrollerComponentView.h +2 -0
- package/ios/RNCNativeScrollerComponentView.mm +42 -0
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +393 -0
- package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +338 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +74 -9
- package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +50 -0
- package/lib/module/CollapsiblePagerView.js +32 -7
- package/lib/module/CollapsiblePagerView.web.js +439 -39
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +13 -0
- package/lib/module/NativeScroller.web.js +16 -3
- package/lib/module/WebCollapsiblePagerPage.js +182 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.native.js +9 -0
- package/lib/module/index.web.js +2 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +10 -3
- package/lib/typescript/src/CollapsiblePagerView.web.d.ts +66 -3
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +5 -0
- package/lib/typescript/src/WebCollapsiblePagerPage.d.ts +42 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.native.d.ts +13 -0
- package/lib/typescript/src/index.web.d.ts +4 -0
- package/package.json +6 -4
- package/react-native-pager-view.podspec +1 -0
- package/src/CollapsiblePagerView.tsx +99 -8
- package/src/CollapsiblePagerView.web.tsx +657 -51
- package/src/CollapsiblePagerViewNativeComponent.ts +13 -0
- package/src/NativeScroller.web.tsx +24 -2
- package/src/WebCollapsiblePagerPage.tsx +258 -0
- package/src/index.native.tsx +30 -0
- package/src/index.tsx +4 -0
- package/src/index.web.tsx +4 -0
package/README.md
CHANGED
|
@@ -66,6 +66,20 @@ replacing its delegate. Android coordinates nested scroll with the active
|
|
|
66
66
|
Web scroll child can opt in by exposing a DOM element with
|
|
67
67
|
`data-collapsible-pager-scroll`. CSS scroll timelines drive header/sticky
|
|
68
68
|
movement, so React is not updated on each vertical scroll frame.
|
|
69
|
+
`pagerRef.current?.scrollHeaderBy(delta)` immediately moves the active page's
|
|
70
|
+
collapsible header by a finite logical distance, clamped to its measured range.
|
|
71
|
+
`scrollHeaderToOffset(offset)` targets an absolute collapse position, so a
|
|
72
|
+
keyboard's own scroll adjustment cannot add a second relative movement.
|
|
73
|
+
It is useful for revealing a focused field above a keyboard.
|
|
74
|
+
|
|
75
|
+
On Web, `WebCollapsiblePagerView` is an explicit root export of the existing Web
|
|
76
|
+
pager. It measures Header and sticky content when their heights are omitted.
|
|
77
|
+
`isActive` describes route visibility; the pager preserves a pending scroll
|
|
78
|
+
target across a short or temporarily hidden page and retries when real content
|
|
79
|
+
geometry becomes ready. `useWebCollapsiblePagerPage()` exposes the selected
|
|
80
|
+
page's scroll owner, visible metrics, content registration and pending target
|
|
81
|
+
to descendants. These Web-only APIs require a DOM when mounted. Native callers
|
|
82
|
+
continue to use `CollapsiblePagerView`.
|
|
69
83
|
|
|
70
84
|
On iOS and Android, `nativeTabBar` renders the tab bar natively and
|
|
71
85
|
`onNativeTabPress` reports presses. By default, adjacent tab presses animate and
|
package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
package com.reactnativepagerview
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.content.pm.ApplicationInfo
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.graphics.Canvas
|
|
8
|
+
import android.graphics.Color
|
|
9
|
+
import android.test.InstrumentationTestCase
|
|
10
|
+
import android.view.MotionEvent
|
|
11
|
+
import android.view.View
|
|
12
|
+
import android.view.ViewGroup
|
|
13
|
+
import android.widget.HorizontalScrollView
|
|
14
|
+
import android.widget.TextView
|
|
15
|
+
import androidx.recyclerview.widget.RecyclerView
|
|
16
|
+
import androidx.viewpager2.widget.ViewPager2
|
|
17
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
18
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
19
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
20
|
+
import kotlin.math.roundToInt
|
|
21
|
+
|
|
22
|
+
class CollapsiblePagerHitTestActivity : Activity()
|
|
23
|
+
|
|
24
|
+
class CollapsiblePagerTabReservationTest : InstrumentationTestCase() {
|
|
25
|
+
fun testSegmentedNativeTabsTrackFractionalProgressAndReset() {
|
|
26
|
+
val context = instrumentation.targetContext
|
|
27
|
+
assertTrue(context.applicationInfo.flags and ApplicationInfo.FLAG_SUPPORTS_RTL != 0)
|
|
28
|
+
val activity = instrumentation.startActivitySync(
|
|
29
|
+
Intent(context, CollapsiblePagerHitTestActivity::class.java)
|
|
30
|
+
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
|
31
|
+
) as CollapsiblePagerHitTestActivity
|
|
32
|
+
val density = context.resources.displayMetrics.density
|
|
33
|
+
fun dp(value: Int) = (value * density).roundToInt()
|
|
34
|
+
lateinit var tab: CollapsiblePagerNativeTabBarView
|
|
35
|
+
lateinit var row: HorizontalScrollView
|
|
36
|
+
lateinit var content: ViewGroup
|
|
37
|
+
lateinit var track: View
|
|
38
|
+
lateinit var thumb: View
|
|
39
|
+
lateinit var buttons: List<TextView>
|
|
40
|
+
fun layout(width: Int = 320, height: Int = 56) {
|
|
41
|
+
tab.measure(View.MeasureSpec.makeMeasureSpec(dp(width), View.MeasureSpec.EXACTLY),
|
|
42
|
+
View.MeasureSpec.makeMeasureSpec(dp(height), View.MeasureSpec.EXACTLY))
|
|
43
|
+
tab.layout(0, 0, dp(width), dp(height))
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
instrumentation.runOnMainSync {
|
|
47
|
+
tab = CollapsiblePagerNativeTabBarView(context)
|
|
48
|
+
activity.setContentView(tab)
|
|
49
|
+
val presses = mutableListOf<String>()
|
|
50
|
+
tab.onItemPress = { _, key -> presses.add(key) }
|
|
51
|
+
tab.updateItemsJSON("""[{"key":"recent","title":"Recents"},""" +
|
|
52
|
+
"""{"key":"account","title":"Accounts"},""" +
|
|
53
|
+
"""{"key":"book","title":"Address Book"}]""")
|
|
54
|
+
tab.updateStyle(56.0, "segmented", 20.0, 12.0, 0.0, 14.0, null, 2.0, 0.0)
|
|
55
|
+
tab.updateColors(Color.LTGRAY, Color.BLACK, Color.GRAY, Color.WHITE)
|
|
56
|
+
layout()
|
|
57
|
+
row = tab.getChildAt(0) as HorizontalScrollView
|
|
58
|
+
content = row.getChildAt(0) as ViewGroup
|
|
59
|
+
track = content.getChildAt(0)
|
|
60
|
+
thumb = content.getChildAt(1)
|
|
61
|
+
buttons = (0 until content.childCount).map(content::getChildAt).filterIsInstance<TextView>()
|
|
62
|
+
assertEquals(3, buttons.size)
|
|
63
|
+
assertEquals(dp(20), track.left)
|
|
64
|
+
assertTrue(kotlin.math.abs(dp(32) - track.height) <= 1)
|
|
65
|
+
assertEquals(0, row.scrollX)
|
|
66
|
+
assertTrue(kotlin.math.abs(buttons[0].width - buttons[1].width) <= 1)
|
|
67
|
+
tab.setProgress(0f)
|
|
68
|
+
val firstX = thumb.left
|
|
69
|
+
tab.setProgress(1f)
|
|
70
|
+
val secondX = thumb.left
|
|
71
|
+
tab.setProgress(0.25f)
|
|
72
|
+
assertTrue(kotlin.math.abs(thumb.left - (firstX + (secondX - firstX) * 0.25f)) <= 1)
|
|
73
|
+
tab.setProgress(0.5f)
|
|
74
|
+
assertTrue(kotlin.math.abs(thumb.left - (firstX + secondX) / 2f) <= 1)
|
|
75
|
+
tab.setProgress(0f)
|
|
76
|
+
assertEquals(firstX, thumb.left)
|
|
77
|
+
buttons[1].performClick()
|
|
78
|
+
assertEquals(listOf("account"), presses)
|
|
79
|
+
|
|
80
|
+
tab.layoutDirection = View.LAYOUT_DIRECTION_RTL
|
|
81
|
+
}
|
|
82
|
+
instrumentation.waitForIdleSync()
|
|
83
|
+
instrumentation.runOnMainSync {
|
|
84
|
+
layout()
|
|
85
|
+
assertEquals(View.LAYOUT_DIRECTION_RTL, row.layoutDirection)
|
|
86
|
+
val rtlFirst = buttons[0].layoutParams as android.widget.FrameLayout.LayoutParams
|
|
87
|
+
val rtlSecond = buttons[1].layoutParams as android.widget.FrameLayout.LayoutParams
|
|
88
|
+
assertTrue(rtlFirst.leftMargin > rtlSecond.leftMargin)
|
|
89
|
+
val rtlGeometry = "rowDir=${row.layoutDirection} contentDir=${content.layoutDirection} " +
|
|
90
|
+
buttons.mapIndexed { index, button ->
|
|
91
|
+
val params = button.layoutParams as android.widget.FrameLayout.LayoutParams
|
|
92
|
+
"$index:${button.left}-${button.right} margin=${params.leftMargin}/${params.rightMargin} gravity=${params.gravity}"
|
|
93
|
+
}.joinToString(" ")
|
|
94
|
+
assertTrue(rtlGeometry, buttons[0].left >= buttons[1].right)
|
|
95
|
+
assertEquals(dp(20), track.left)
|
|
96
|
+
tab.setProgress(0f)
|
|
97
|
+
val rtlFirstX = thumb.left
|
|
98
|
+
tab.setProgress(1f)
|
|
99
|
+
val rtlSecondX = thumb.left
|
|
100
|
+
assertTrue(rtlFirstX > rtlSecondX)
|
|
101
|
+
tab.setProgress(0.5f)
|
|
102
|
+
assertTrue(kotlin.math.abs(thumb.left - (rtlFirstX + rtlSecondX) / 2f) <= 1)
|
|
103
|
+
tab.layoutDirection = View.LAYOUT_DIRECTION_LTR
|
|
104
|
+
}
|
|
105
|
+
instrumentation.waitForIdleSync()
|
|
106
|
+
instrumentation.runOnMainSync {
|
|
107
|
+
layout()
|
|
108
|
+
assertTrue(buttons[0].right <= buttons[1].left)
|
|
109
|
+
tab.updateItemsJSON("""[{"key":"recent","title":"Recents (8)"},""" +
|
|
110
|
+
"""{"key":"account","title":"Accounts"}]""")
|
|
111
|
+
layout()
|
|
112
|
+
val twoButtons = (0 until content.childCount).map(content::getChildAt).filterIsInstance<TextView>()
|
|
113
|
+
assertEquals(2, twoButtons.size)
|
|
114
|
+
assertTrue(kotlin.math.abs(twoButtons[0].width - track.width / 2) <= 1)
|
|
115
|
+
layout(width = 30)
|
|
116
|
+
assertEquals(dp(15), track.left)
|
|
117
|
+
assertEquals(0, track.width)
|
|
118
|
+
tab.updateStyle(56.0, "segmented", 20.0, 40.0, 0.0, 14.0, null, 2.0, 0.0)
|
|
119
|
+
layout(width = 30)
|
|
120
|
+
assertTrue(kotlin.math.abs(dp(28) - track.top) <= 1)
|
|
121
|
+
assertEquals(0, track.height)
|
|
122
|
+
assertEquals(View.GONE, thumb.visibility)
|
|
123
|
+
tab.updateItemsJSON("[]")
|
|
124
|
+
assertEquals(View.GONE, tab.visibility)
|
|
125
|
+
tab.updateItemsJSON("""[{"key":"recent","title":"Recents"}]""")
|
|
126
|
+
tab.updateStyle(44.0, "underline", 20.0, 0.0, 8.0, 16.0, null, 2.0, 0.0)
|
|
127
|
+
layout(height = 44)
|
|
128
|
+
assertEquals(View.GONE, track.visibility)
|
|
129
|
+
assertEquals(dp(2), thumb.height)
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
instrumentation.runOnMainSync { activity.finish() }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fun testTabViewportToolbarHitAndResetInBothDirections() {
|
|
137
|
+
val context = instrumentation.targetContext
|
|
138
|
+
assertTrue(context.applicationInfo.flags and ApplicationInfo.FLAG_SUPPORTS_RTL != 0)
|
|
139
|
+
val activity = instrumentation.startActivitySync(
|
|
140
|
+
Intent(context, CollapsiblePagerHitTestActivity::class.java)
|
|
141
|
+
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
|
142
|
+
) as CollapsiblePagerHitTestActivity
|
|
143
|
+
val toolbarPresses = AtomicInteger()
|
|
144
|
+
val tabPresses = AtomicInteger()
|
|
145
|
+
try {
|
|
146
|
+
instrumentation.runOnMainSync {
|
|
147
|
+
val density = context.resources.displayMetrics.density
|
|
148
|
+
fun dp(value: Int) = (value * density).roundToInt()
|
|
149
|
+
val width = dp(320)
|
|
150
|
+
val height = dp(500)
|
|
151
|
+
val reserved = dp(64)
|
|
152
|
+
val tabHeight = dp(44)
|
|
153
|
+
val host = CollapsiblePagerHost(context)
|
|
154
|
+
activity.setContentView(host)
|
|
155
|
+
assertTrue(host.isAttachedToWindow)
|
|
156
|
+
val sticky = ReactViewGroup(context).apply { pointerEvents = PointerEvents.BOX_NONE }
|
|
157
|
+
val fade = View(context).apply { setBackgroundColor(Color.RED) }
|
|
158
|
+
val toolbar = View(context).apply {
|
|
159
|
+
setBackgroundColor(Color.BLUE)
|
|
160
|
+
isClickable = true
|
|
161
|
+
}
|
|
162
|
+
sticky.addView(fade)
|
|
163
|
+
sticky.addView(toolbar)
|
|
164
|
+
host.addReactChild(View(context), 0)
|
|
165
|
+
host.addReactChild(sticky, 1)
|
|
166
|
+
host.headerHeightPx = 0
|
|
167
|
+
host.stickyHeaderHeightPx = tabHeight
|
|
168
|
+
host.nativeTabBarBackgroundColor = Color.GREEN
|
|
169
|
+
host.applyNativeHeaderStyle()
|
|
170
|
+
host.updateNativeTabBarItems(
|
|
171
|
+
"""[{"key":"a","title":"Overview"},{"key":"b","title":"Ethereum"},""" +
|
|
172
|
+
"""{"key":"c","title":"BNB Chain"},{"key":"d","title":"Polygon"}]""",
|
|
173
|
+
)
|
|
174
|
+
val tab = (0 until host.childCount).map(host::getChildAt)
|
|
175
|
+
.first { it.javaClass.simpleName == "CollapsiblePagerNativeTabBarView" } as ViewGroup
|
|
176
|
+
val row = tab.getChildAt(0) as HorizontalScrollView
|
|
177
|
+
toolbar.setOnClickListener { toolbarPresses.incrementAndGet() }
|
|
178
|
+
host.onNativeTabPress = { _, _ -> tabPresses.incrementAndGet() }
|
|
179
|
+
|
|
180
|
+
fun layout() {
|
|
181
|
+
host.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
|
182
|
+
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))
|
|
183
|
+
host.layout(0, 0, width, height)
|
|
184
|
+
}
|
|
185
|
+
fun tap(x: Int) {
|
|
186
|
+
val time = android.os.SystemClock.uptimeMillis()
|
|
187
|
+
val down = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN,
|
|
188
|
+
x.toFloat(), (tabHeight / 2).toFloat(), 0)
|
|
189
|
+
val up = MotionEvent.obtain(time, time + 20, MotionEvent.ACTION_UP,
|
|
190
|
+
x.toFloat(), (tabHeight / 2).toFloat(), 0)
|
|
191
|
+
host.dispatchTouchEvent(down)
|
|
192
|
+
host.dispatchTouchEvent(up)
|
|
193
|
+
down.recycle()
|
|
194
|
+
up.recycle()
|
|
195
|
+
}
|
|
196
|
+
fun colorAt(x: Int): Int {
|
|
197
|
+
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
198
|
+
host.draw(Canvas(bitmap))
|
|
199
|
+
val color = bitmap.getPixel(x, tabHeight / 2)
|
|
200
|
+
bitmap.recycle()
|
|
201
|
+
return color
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
host.nativeTabBarTrailingReservedWidth = 64.0
|
|
205
|
+
layout()
|
|
206
|
+
fade.layout(width - reserved - dp(20), 0, width - reserved, tabHeight)
|
|
207
|
+
toolbar.layout(width - reserved, 0, width, tabHeight)
|
|
208
|
+
assertEquals(width - reserved, tab.width)
|
|
209
|
+
assertEquals(0, tab.left)
|
|
210
|
+
assertEquals(width, sticky.width)
|
|
211
|
+
assertEquals(Color.RED, colorAt(width - reserved - dp(10)))
|
|
212
|
+
tap(width - dp(10))
|
|
213
|
+
tap(dp(28))
|
|
214
|
+
host.updateNativeTabProgress(3, 0f)
|
|
215
|
+
val content = row.getChildAt(0) as ViewGroup
|
|
216
|
+
val lastTab = (0 until content.childCount).map(content::getChildAt)
|
|
217
|
+
.filterIsInstance<TextView>().last()
|
|
218
|
+
assertTrue(lastTab.left >= row.scrollX - dp(1))
|
|
219
|
+
assertTrue(lastTab.right <= row.scrollX + row.width + dp(1))
|
|
220
|
+
|
|
221
|
+
host.setPagerLayoutDirection(View.LAYOUT_DIRECTION_RTL)
|
|
222
|
+
host.nativeTabBarTrailingReservedWidth = 64.0
|
|
223
|
+
layout()
|
|
224
|
+
assertEquals(View.LAYOUT_DIRECTION_RTL, tab.layoutDirection)
|
|
225
|
+
toolbar.layout(0, 0, reserved, tabHeight)
|
|
226
|
+
fade.layout(reserved, 0, reserved + dp(20), tabHeight)
|
|
227
|
+
assertEquals(reserved, tab.left)
|
|
228
|
+
assertEquals(width, tab.right)
|
|
229
|
+
assertEquals(Color.RED, colorAt(reserved + dp(10)))
|
|
230
|
+
tap(dp(10))
|
|
231
|
+
|
|
232
|
+
host.nativeTabBarTrailingReservedWidth = 500.0
|
|
233
|
+
layout()
|
|
234
|
+
assertEquals(0, tab.width)
|
|
235
|
+
host.nativeTabBarTrailingReservedWidth = 0.0
|
|
236
|
+
layout()
|
|
237
|
+
assertEquals(0, tab.left)
|
|
238
|
+
assertEquals(width, tab.width)
|
|
239
|
+
assertTrue(host.indexOfChild(sticky) < host.indexOfChild(tab))
|
|
240
|
+
}
|
|
241
|
+
instrumentation.waitForIdleSync()
|
|
242
|
+
assertEquals(2, toolbarPresses.get())
|
|
243
|
+
assertEquals(1, tabPresses.get())
|
|
244
|
+
} finally {
|
|
245
|
+
instrumentation.runOnMainSync { activity.finish() }
|
|
246
|
+
instrumentation.waitForIdleSync()
|
|
247
|
+
}
|
|
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
|
+
}
|
|
306
|
+
}
|
|
@@ -142,7 +142,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
142
142
|
private var transitionSourceRecyclerAnchor: RecyclerReadingAnchor? = null
|
|
143
143
|
private val commandBodyKeys = HashSet<String>()
|
|
144
144
|
private var explicitTopCommand: Pair<String, PageRestoreRequest>? = null
|
|
145
|
-
private class PagerTopRequest(view: View, val key: String, val pageIndex: Int, val generation: Int, val
|
|
145
|
+
private class PagerTopRequest(view: View, val key: String, val pageIndex: Int, val generation: Int, val logicalOffset: Int, val token: Int) {
|
|
146
146
|
val view = WeakReference(view)
|
|
147
147
|
}
|
|
148
148
|
private var pagerTopRequest: PagerTopRequest? = null
|
|
@@ -293,7 +293,23 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
293
293
|
var onNativeSubHeaderPress: ((Int, String) -> Unit)? = null
|
|
294
294
|
|
|
295
295
|
var nativeTabBarHeight = 44.0
|
|
296
|
+
var nativeTabBarVariant = "underline"
|
|
297
|
+
var nativeTabBarTrailingReservedWidth = 0.0
|
|
298
|
+
set(value) {
|
|
299
|
+
field = if (value.isFinite() && value >= 0.0) value else 0.0
|
|
300
|
+
bringNativeHeadersToFront()
|
|
301
|
+
requestLayout()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private fun nativeTabReservationPx(width: Int): Int {
|
|
305
|
+
val reserved = nativeTabBarTrailingReservedWidth
|
|
306
|
+
val density = resources.displayMetrics.density
|
|
307
|
+
return if (reserved.isFinite() && reserved > 0.0 && width > 0 && density > 0f) {
|
|
308
|
+
(reserved.coerceAtMost(width / density.toDouble()) * density).roundToInt()
|
|
309
|
+
} else 0
|
|
310
|
+
}
|
|
296
311
|
var nativeTabBarContentPaddingHorizontal = 20.0
|
|
312
|
+
var nativeTabBarContentPaddingVertical = 0.0
|
|
297
313
|
var nativeTabBarItemSpacing = 8.0
|
|
298
314
|
var nativeTabBarFontSize = 16.0
|
|
299
315
|
var nativeTabBarFontFamily: String? = null
|
|
@@ -311,6 +327,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
311
327
|
pager.id = View.generateViewId()
|
|
312
328
|
pager.adapter = adapter
|
|
313
329
|
pager.isSaveEnabled = false
|
|
330
|
+
(pager.getChildAt(0) as? RecyclerView)?.itemAnimator = null
|
|
314
331
|
super.addView(
|
|
315
332
|
pager,
|
|
316
333
|
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
|
|
@@ -416,7 +433,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
416
433
|
fun applyNativeHeaderStyle() {
|
|
417
434
|
nativeTabBarView.updateStyle(
|
|
418
435
|
nativeTabBarHeight,
|
|
436
|
+
nativeTabBarVariant,
|
|
419
437
|
nativeTabBarContentPaddingHorizontal,
|
|
438
|
+
nativeTabBarContentPaddingVertical,
|
|
420
439
|
nativeTabBarItemSpacing,
|
|
421
440
|
nativeTabBarFontSize,
|
|
422
441
|
nativeTabBarFontFamily,
|
|
@@ -442,6 +461,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
442
461
|
private fun bringNativeHeadersToFront() {
|
|
443
462
|
nativeTabBarView.bringToFront()
|
|
444
463
|
nativeSubHeaderView.bringToFront()
|
|
464
|
+
if (nativeTabBarTrailingReservedWidth > 0.0) stickyHeaderView?.bringToFront()
|
|
445
465
|
}
|
|
446
466
|
|
|
447
467
|
fun addReactChild(child: View, index: Int) {
|
|
@@ -689,7 +709,16 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
689
709
|
|
|
690
710
|
private fun layoutPagerIfRequested() {
|
|
691
711
|
val recycler = pager.getChildAt(0) as? RecyclerView ?: return
|
|
692
|
-
if (
|
|
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
|
|
693
722
|
// OneKey patch: a non-animated page command queues RecyclerView layout, but RN
|
|
694
723
|
// can stop that request after navigation. Drain it before showing the page.
|
|
695
724
|
recycler.measure(
|
|
@@ -712,6 +741,13 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
712
741
|
layout(left, top, right, bottom)
|
|
713
742
|
}
|
|
714
743
|
|
|
744
|
+
private fun isPagerRecyclerLayoutSuppressed(): Boolean =
|
|
745
|
+
shouldSuppressCollapsiblePagerRecyclerLayout(
|
|
746
|
+
(pager.getChildAt(0) as? RecyclerView)?.isLayoutSuppressed == true,
|
|
747
|
+
adapter.itemCount,
|
|
748
|
+
attachedPageCount(),
|
|
749
|
+
)
|
|
750
|
+
|
|
715
751
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
716
752
|
val width = MeasureSpec.getSize(widthMeasureSpec)
|
|
717
753
|
val height = MeasureSpec.getSize(heightMeasureSpec)
|
|
@@ -722,10 +758,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
722
758
|
return
|
|
723
759
|
}
|
|
724
760
|
val pagerHeight = height + headerHeightPx
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
761
|
+
if (!isPagerRecyclerLayoutSuppressed()) {
|
|
762
|
+
pager.measure(
|
|
763
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
764
|
+
MeasureSpec.makeMeasureSpec(pagerHeight, MeasureSpec.EXACTLY),
|
|
765
|
+
)
|
|
766
|
+
}
|
|
729
767
|
headerView?.measure(
|
|
730
768
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
731
769
|
MeasureSpec.makeMeasureSpec(headerHeightPx, MeasureSpec.EXACTLY),
|
|
@@ -737,8 +775,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
737
775
|
val nativeTabHeight = if (nativeTabBarView.visibility == View.VISIBLE) {
|
|
738
776
|
nativeTabBarView.preferredHeightPx()
|
|
739
777
|
} else 0
|
|
778
|
+
val tabReservedWidth = nativeTabReservationPx(width)
|
|
740
779
|
nativeTabBarView.measure(
|
|
741
|
-
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
780
|
+
MeasureSpec.makeMeasureSpec(width - tabReservedWidth, MeasureSpec.EXACTLY),
|
|
742
781
|
MeasureSpec.makeMeasureSpec(nativeTabHeight, MeasureSpec.EXACTLY),
|
|
743
782
|
)
|
|
744
783
|
val nativeSubHeaderHeight = if (nativeSubHeaderView.visibility == View.VISIBLE) {
|
|
@@ -757,7 +796,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
757
796
|
// RN ancestors allow overflow; keep translated header content inside this viewport.
|
|
758
797
|
clipBounds = Rect(0, 0, width, height)
|
|
759
798
|
if (width <= 0 || height <= 0) return
|
|
760
|
-
|
|
799
|
+
if (!isPagerRecyclerLayoutSuppressed()) {
|
|
800
|
+
pager.layout(0, 0, width, height + headerHeightPx)
|
|
801
|
+
}
|
|
761
802
|
headerView?.layout(0, 0, width, headerHeightPx)
|
|
762
803
|
stickyHeaderView?.layout(
|
|
763
804
|
0,
|
|
@@ -766,7 +807,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
766
807
|
headerHeightPx + stickyHeaderHeightPx,
|
|
767
808
|
)
|
|
768
809
|
val nativeTabHeight = nativeTabBarView.measuredHeight
|
|
769
|
-
|
|
810
|
+
val tabReservedWidth = nativeTabReservationPx(width)
|
|
811
|
+
val tabLeft = if (nativeTabBarView.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
|
812
|
+
tabReservedWidth
|
|
813
|
+
} else 0
|
|
814
|
+
nativeTabBarView.layout(tabLeft, headerHeightPx, tabLeft + width - tabReservedWidth,
|
|
815
|
+
headerHeightPx + nativeTabHeight)
|
|
770
816
|
nativeSubHeaderView.layout(
|
|
771
817
|
0,
|
|
772
818
|
headerHeightPx + nativeTabHeight,
|
|
@@ -828,7 +874,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
828
874
|
}
|
|
829
875
|
if (!reached()) {
|
|
830
876
|
if (!isAttachedToWindow || !view.isAttachedToWindow || width <= 0 || height <= 0 ||
|
|
831
|
-
view.width <= 0 || view.height <= 0 || isInLayout || view.isInLayout || view.isComputingLayout ||
|
|
877
|
+
view.width <= 0 || view.height <= 0 || isInLayout || view.isInLayout || view.isLayoutSuppressed || view.isComputingLayout ||
|
|
832
878
|
pageKey(index) != key || primaryScrollableForPage(index) !== view || !isCurrentPageRestore(key, request)) return false
|
|
833
879
|
val previousSynthetic = syntheticPageRestore
|
|
834
880
|
syntheticPageRestore = true
|
|
@@ -987,13 +1033,33 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
987
1033
|
if (!settled) return
|
|
988
1034
|
pageOffsets[request.key] = 0
|
|
989
1035
|
if (view is RecyclerView) saveRecyclerReadingAnchor(request.key, view)
|
|
990
|
-
setHeaderOffset(
|
|
991
|
-
offsetDiag("pager-top-complete-
|
|
1036
|
+
setHeaderOffset(request.logicalOffset.coerceIn(0, headerHeightPx))
|
|
1037
|
+
offsetDiag("pager-top-complete-offset=${request.logicalOffset}-token=${request.token}", view, request.key)
|
|
992
1038
|
cancelPagerTop()
|
|
993
1039
|
}
|
|
994
1040
|
|
|
995
1041
|
internal fun scrollToTop(target: Int, animated: Boolean) {
|
|
996
|
-
if (target !in 0..1
|
|
1042
|
+
if (target !in 0..1) return
|
|
1043
|
+
scrollHeaderToOffset(if (target == 1) headerHeightPx else 0, animated)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
internal fun scrollHeaderBy(delta: Double) {
|
|
1047
|
+
if (!delta.isFinite()) return
|
|
1048
|
+
val density = resources.displayMetrics.density.toDouble()
|
|
1049
|
+
val maxDip = headerHeightPx / density
|
|
1050
|
+
val deltaPx = (delta.coerceIn(-maxDip, maxDip) * density).roundToInt()
|
|
1051
|
+
scrollHeaderToOffset((headerOffsetPx.toLong() + deltaPx).coerceIn(0, headerHeightPx.toLong()).toInt(), false)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
internal fun scrollHeaderToOffset(offset: Double) {
|
|
1055
|
+
if (!offset.isFinite()) return
|
|
1056
|
+
val density = resources.displayMetrics.density.toDouble()
|
|
1057
|
+
val targetPx = (offset.coerceIn(0.0, headerHeightPx / density) * density).roundToInt()
|
|
1058
|
+
scrollHeaderToOffset(targetPx, false)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private fun scrollHeaderToOffset(logicalOffset: Int, animated: Boolean) {
|
|
1062
|
+
if (pager.scrollState != ViewPager2.SCROLL_STATE_IDLE || transitionHeaderOffsets != null ||
|
|
997
1063
|
syntheticPageRestore || selectedPage !in 0 until adapter.itemCount || !isAttachedToWindow) return
|
|
998
1064
|
val view = observedRecyclerView ?: observedOwnedScroller ?: observedNativeScroller ?: return
|
|
999
1065
|
val key = currentPageKey()
|
|
@@ -1020,11 +1086,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1020
1086
|
if (view is RecyclerView) deferredRecyclerGeometry.remove(view)
|
|
1021
1087
|
if (view is ScrollView) deferredScrollerGeometry.remove(view)
|
|
1022
1088
|
pageOffsets[key] = 0
|
|
1023
|
-
val
|
|
1089
|
+
val targetOffset = logicalOffset.coerceIn(0, headerHeightPx)
|
|
1090
|
+
val request = PagerTopRequest(view, key, selectedPage, attachmentGeneration, targetOffset, ++pagerTopToken)
|
|
1024
1091
|
pagerTopRequest = request
|
|
1025
1092
|
commandBodyKeys.add(key)
|
|
1026
|
-
setHeaderOffset(
|
|
1027
|
-
offsetDiag("pager-top-begin-
|
|
1093
|
+
setHeaderOffset(targetOffset)
|
|
1094
|
+
offsetDiag("pager-top-begin-offset=$targetOffset-animated=$animated-token=${request.token}", view, key)
|
|
1028
1095
|
dispatchingPagerTop = true
|
|
1029
1096
|
try {
|
|
1030
1097
|
when (view) {
|
|
@@ -1033,7 +1100,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1033
1100
|
if (animated && !recyclerIsAtBodyTop(view)) view.smoothScrollToPosition(0)
|
|
1034
1101
|
else if (manager != null && manager.orientation == RecyclerView.VERTICAL) {
|
|
1035
1102
|
manager.scrollToPositionWithOffset(0, 0)
|
|
1036
|
-
if (view.width > 0 && view.height > 0 && !isInLayout && !view.isInLayout && !view.isComputingLayout) {
|
|
1103
|
+
if (view.width > 0 && view.height > 0 && !isInLayout && !view.isInLayout && !view.isLayoutSuppressed && !view.isComputingLayout) {
|
|
1037
1104
|
view.measure(MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(view.height, MeasureSpec.EXACTLY))
|
|
1038
1105
|
view.layout(view.left, view.top, view.right, view.bottom)
|
|
1039
1106
|
}
|
|
@@ -1638,7 +1705,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1638
1705
|
if (anchorOffset != null) manager?.scrollToPositionWithOffset(first, anchorOffset)
|
|
1639
1706
|
// React Native can stop this nested requestLayout at its React-owned parent.
|
|
1640
1707
|
// Commit the changed inset before drawing instead of waiting for a data update.
|
|
1641
|
-
if (recycler.width > 0 && recycler.height > 0 && !recycler.isComputingLayout) {
|
|
1708
|
+
if (recycler.width > 0 && recycler.height > 0 && !recycler.isLayoutSuppressed && !recycler.isComputingLayout) {
|
|
1642
1709
|
recycler.forceLayout()
|
|
1643
1710
|
recycler.measure(
|
|
1644
1711
|
MeasureSpec.makeMeasureSpec(recycler.width, MeasureSpec.EXACTLY),
|
|
@@ -2012,14 +2079,16 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2012
2079
|
}
|
|
2013
2080
|
}
|
|
2014
2081
|
|
|
2015
|
-
private fun headerRegionAt(y: Float): String? {
|
|
2082
|
+
private fun headerRegionAt(x: Float, y: Float): String? {
|
|
2016
2083
|
val headerBottom = headerHeightPx - headerOffsetPx
|
|
2017
2084
|
val stickyBottom = headerHeightPx + stickyHeaderHeightPx - headerOffsetPx
|
|
2018
2085
|
return when {
|
|
2019
2086
|
y < 0 || y >= stickyBottom -> null
|
|
2020
2087
|
y < headerBottom -> "header"
|
|
2021
2088
|
y < headerBottom + nativeTabBarView.measuredHeight &&
|
|
2022
|
-
nativeTabBarView.visibility == View.VISIBLE ->
|
|
2089
|
+
nativeTabBarView.visibility == View.VISIBLE ->
|
|
2090
|
+
if (x >= nativeTabBarView.left && x < nativeTabBarView.right) "primary-tab"
|
|
2091
|
+
else "sticky-controls"
|
|
2023
2092
|
y < headerBottom + nativeTabBarView.measuredHeight + nativeSubHeaderView.measuredHeight &&
|
|
2024
2093
|
nativeSubHeaderView.visibility == View.VISIBLE -> "secondary-header"
|
|
2025
2094
|
else -> "sticky-controls"
|
|
@@ -2128,7 +2197,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
2128
2197
|
pressCancelled = false
|
|
2129
2198
|
nativeGestureStarted = false
|
|
2130
2199
|
nestedNativeGestureStarted = false
|
|
2131
|
-
headerTouchRegion = headerRegionAt(event.y) ?: "none"
|
|
2200
|
+
headerTouchRegion = headerRegionAt(event.x, event.y) ?: "none"
|
|
2132
2201
|
headerTouchActive = headerTouchRegion != "none"
|
|
2133
2202
|
if (!headerTouchActive) return super.dispatchTouchEvent(event)
|
|
2134
2203
|
headerDownX = event.x
|