@onekeyfe/react-native-pager-view 3.0.157-alpha.255 → 3.0.157-alpha.257
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/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +52 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +20 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +58 -0
- package/docs/SPEC.md +59 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +162 -0
- package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +180 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import android.view.ViewConfiguration
|
|
|
9
9
|
import android.view.ViewGroup
|
|
10
10
|
import android.view.ViewTreeObserver
|
|
11
11
|
import android.widget.HorizontalScrollView
|
|
12
|
+
import android.widget.ImageView
|
|
12
13
|
import android.widget.ScrollView
|
|
13
14
|
// OneKey patch: FrameLayout is inherited through NestedScrollableHost.
|
|
14
15
|
// import android.widget.FrameLayout
|
|
@@ -17,11 +18,14 @@ import androidx.core.view.NestedScrollingParentHelper
|
|
|
17
18
|
import androidx.core.view.ViewCompat
|
|
18
19
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
19
20
|
import androidx.recyclerview.widget.RecyclerView
|
|
21
|
+
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
|
20
22
|
import androidx.viewpager2.widget.ViewPager2
|
|
21
23
|
import com.facebook.react.R as ReactR
|
|
22
24
|
import com.facebook.react.uimanager.events.NativeGestureUtil
|
|
23
25
|
import com.facebook.react.views.scroll.ReactScrollView
|
|
26
|
+
import com.facebook.react.views.swiperefresh.ReactSwipeRefreshLayout
|
|
24
27
|
import com.margelo.nitro.nativelogger.OneKeyLog
|
|
28
|
+
import java.lang.ref.WeakReference
|
|
25
29
|
import java.util.WeakHashMap
|
|
26
30
|
import kotlin.math.max
|
|
27
31
|
import kotlin.math.min
|
|
@@ -116,6 +120,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
116
120
|
private val restoredRecyclerKeys = WeakHashMap<RecyclerView, String>()
|
|
117
121
|
private val originalScrollerPadding = WeakHashMap<ScrollView, ScrollerPadding>()
|
|
118
122
|
private val restoredScrollerKeys = WeakHashMap<ScrollView, String>()
|
|
123
|
+
private val scrollerRefreshIndicators = WeakHashMap<ScrollView, WeakReference<View>>()
|
|
119
124
|
private val pageOffsets = HashMap<String, Int>()
|
|
120
125
|
private var observedRecyclerView: RecyclerView? = null
|
|
121
126
|
private var observedScrollListener: RecyclerView.OnScrollListener? = null
|
|
@@ -604,6 +609,12 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
604
609
|
}
|
|
605
610
|
|
|
606
611
|
private fun prepareAdjacentPages() {
|
|
612
|
+
// Retained pages and replaced controls may leave the attached view tree.
|
|
613
|
+
for ((scroller, indicator) in scrollerRefreshIndicators.toMap()) {
|
|
614
|
+
if (nativeScrollerRefreshIndicator(scroller) !== indicator.get()) {
|
|
615
|
+
restoreNativeScrollerRefreshIndicator(scroller)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
607
618
|
val first = max(0, selectedPage - 1)
|
|
608
619
|
val last = min(adapter.itemCount - 1, selectedPage + 1)
|
|
609
620
|
for (index in first..last) {
|
|
@@ -794,6 +805,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
794
805
|
updateScrollerPaddingOwnership(scroller, original)
|
|
795
806
|
val topInset = headerHeightPx + stickyHeaderHeightPx
|
|
796
807
|
scroller.clipToPadding = false
|
|
808
|
+
applyNativeScrollerRefreshIndicator(scroller, topInset)
|
|
797
809
|
if (scroller is ReactScrollView) {
|
|
798
810
|
// Fabric owns the content child's layout, so ScrollView padding alone
|
|
799
811
|
// cannot move it. RN's native ScrollAway API also updates Fabric culling.
|
|
@@ -896,6 +908,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
896
908
|
}
|
|
897
909
|
|
|
898
910
|
private fun restoreNativeScrollerInsets(scroller: ScrollView) {
|
|
911
|
+
restoreNativeScrollerRefreshIndicator(scroller)
|
|
899
912
|
val original = originalScrollerPadding.remove(scroller) ?: return
|
|
900
913
|
updateScrollerPaddingOwnership(scroller, original)
|
|
901
914
|
restoredScrollerKeys.remove(scroller)
|
|
@@ -910,6 +923,44 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
910
923
|
scroller.setPadding(original.left, original.top, original.right, original.bottom)
|
|
911
924
|
}
|
|
912
925
|
|
|
926
|
+
private fun nativeScrollerRefreshIndicator(scroller: ScrollView): View? {
|
|
927
|
+
if (!nativeSmoothHeaderScrollEnabled) return null
|
|
928
|
+
var ancestor = scroller.parent
|
|
929
|
+
while (ancestor != null && ancestor !== this) ancestor = ancestor.parent
|
|
930
|
+
if (ancestor !== this) return null
|
|
931
|
+
val refresh = scroller.parent as? ReactSwipeRefreshLayout ?: return null
|
|
932
|
+
for (index in 0 until refresh.childCount) {
|
|
933
|
+
val child = refresh.getChildAt(index)
|
|
934
|
+
if (child is ImageView && child.drawable is CircularProgressDrawable) return child
|
|
935
|
+
}
|
|
936
|
+
return null
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private fun applyNativeScrollerRefreshIndicator(scroller: ScrollView, inset: Int) {
|
|
940
|
+
val indicator = nativeScrollerRefreshIndicator(scroller)
|
|
941
|
+
if (scrollerRefreshIndicators[scroller]?.get() !== indicator) {
|
|
942
|
+
restoreNativeScrollerRefreshIndicator(scroller)
|
|
943
|
+
}
|
|
944
|
+
if (indicator == null) return
|
|
945
|
+
if (scrollerRefreshIndicators[scroller]?.get() !== indicator) {
|
|
946
|
+
scrollerRefreshIndicators[scroller] = WeakReference(indicator)
|
|
947
|
+
}
|
|
948
|
+
val translation = refreshIndicatorTranslations.getOrPut(indicator) {
|
|
949
|
+
NativeScrollerRefreshTranslation(indicator.translationY)
|
|
950
|
+
}
|
|
951
|
+
// Moving only the drawing preserves RN's offsets and AndroidX's active pull/refresh animation.
|
|
952
|
+
// setProgressViewOffset would reset that animation and clear isRefreshing.
|
|
953
|
+
val next = translation.update(indicator.translationY, this, inset.toFloat())
|
|
954
|
+
if (indicator.translationY != next) indicator.translationY = next
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private fun restoreNativeScrollerRefreshIndicator(scroller: ScrollView) {
|
|
958
|
+
val indicator = scrollerRefreshIndicators.remove(scroller)?.get() ?: return
|
|
959
|
+
val translation = refreshIndicatorTranslations[indicator] ?: return
|
|
960
|
+
indicator.translationY = translation.update(indicator.translationY, this, null)
|
|
961
|
+
if (translation.isEmpty) refreshIndicatorTranslations.remove(indicator)
|
|
962
|
+
}
|
|
963
|
+
|
|
913
964
|
private fun headerRegionAt(y: Float): String? {
|
|
914
965
|
val headerBottom = headerHeightPx - headerOffsetPx
|
|
915
966
|
val stickyBottom = headerHeightPx + stickyHeaderHeightPx - headerOffsetPx
|
|
@@ -1232,6 +1283,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
1232
1283
|
}
|
|
1233
1284
|
|
|
1234
1285
|
companion object {
|
|
1286
|
+
private val refreshIndicatorTranslations = WeakHashMap<View, NativeScrollerRefreshTranslation>()
|
|
1235
1287
|
const val NATIVE_SCROLLER_ID_PREFIX = "rnc-collapsible-pager-native-scroller:"
|
|
1236
1288
|
const val PAGE_SLOT_OFFSET = 2
|
|
1237
1289
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.reactnativepagerview
|
|
2
2
|
|
|
3
|
+
import java.util.WeakHashMap
|
|
4
|
+
|
|
3
5
|
// Replace only this pager's contribution; another native owner may share the scroller.
|
|
4
6
|
internal fun nativeScrollerScrollAwayTop(currentTop: Int, previousInset: Int, nextInset: Int): Int =
|
|
5
7
|
(currentTop - previousInset).coerceAtLeast(0) + nextInset
|
|
@@ -15,3 +17,21 @@ internal fun nativeScrollerPaddingWasResetByScrollAway(
|
|
|
15
17
|
scrollAwayBottom: Int,
|
|
16
18
|
): Boolean = appliedInset > 0 && left == 0 && top == 0 && right == 0 &&
|
|
17
19
|
bottom == scrollAwayTop + scrollAwayBottom
|
|
20
|
+
|
|
21
|
+
internal class NativeScrollerRefreshTranslation(initialTranslation: Float) {
|
|
22
|
+
private val contributions = WeakHashMap<Any, Float>()
|
|
23
|
+
private var baseline = initialTranslation
|
|
24
|
+
private var lastApplied = initialTranslation
|
|
25
|
+
|
|
26
|
+
val isEmpty: Boolean
|
|
27
|
+
get() = contributions.isEmpty()
|
|
28
|
+
|
|
29
|
+
fun update(currentTranslation: Float, owner: Any, inset: Float?): Float {
|
|
30
|
+
// Native refresh layout owns top/scale, while an external translation is
|
|
31
|
+
// an absolute baseline rather than another copy of our previous inset.
|
|
32
|
+
if (currentTranslation != lastApplied) baseline = currentTranslation
|
|
33
|
+
if (inset == null) contributions.remove(owner) else contributions[owner] = inset
|
|
34
|
+
lastApplied = baseline + contributions.values.sum()
|
|
35
|
+
return lastApplied
|
|
36
|
+
}
|
|
37
|
+
}
|
package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt
CHANGED
|
@@ -34,4 +34,62 @@ fun main() {
|
|
|
34
34
|
// Initial discovery must capture caller values before this pager owns an inset.
|
|
35
35
|
check(!nativeScrollerPaddingWasResetByScrollAway(0, 0, 0, 0, 230, 230, 0))
|
|
36
36
|
println("NativeScroller ScrollAway ownership: initial, repeat, measured resize, nested detach, Fabric reset, RN padding replay, and caller updates passed")
|
|
37
|
+
|
|
38
|
+
val firstOwner = Any()
|
|
39
|
+
val secondOwner = Any()
|
|
40
|
+
val translation = NativeScrollerRefreshTranslation(7.5f)
|
|
41
|
+
check(translation.isEmpty)
|
|
42
|
+
var current = translation.update(7.5f, firstOwner, 230f)
|
|
43
|
+
check(current == 237.5f)
|
|
44
|
+
check(!translation.isEmpty)
|
|
45
|
+
current = translation.update(current, firstOwner, 230f)
|
|
46
|
+
check(current == 237.5f)
|
|
47
|
+
current = translation.update(current, firstOwner, 358f)
|
|
48
|
+
check(current == 365.5f)
|
|
49
|
+
current = translation.update(current, secondOwner, 64f)
|
|
50
|
+
check(current == 429.5f)
|
|
51
|
+
current = translation.update(current, firstOwner, null)
|
|
52
|
+
check(current == 71.5f)
|
|
53
|
+
check(!translation.isEmpty)
|
|
54
|
+
current = translation.update(current, secondOwner, null)
|
|
55
|
+
check(current == 7.5f)
|
|
56
|
+
check(translation.isEmpty)
|
|
57
|
+
|
|
58
|
+
// Reversing release order must restore the same original translation.
|
|
59
|
+
current = translation.update(current, firstOwner, 230f)
|
|
60
|
+
current = translation.update(current, secondOwner, 64f)
|
|
61
|
+
current = translation.update(current, secondOwner, null)
|
|
62
|
+
check(current == 237.5f)
|
|
63
|
+
current = translation.update(current, firstOwner, null)
|
|
64
|
+
check(current == 7.5f)
|
|
65
|
+
check(translation.isEmpty)
|
|
66
|
+
|
|
67
|
+
// A caller's absolute translation update replaces the baseline, not its inset.
|
|
68
|
+
current = translation.update(current, firstOwner, 230f)
|
|
69
|
+
check(current == 237.5f)
|
|
70
|
+
current = translation.update(-12.5f, firstOwner, 230f)
|
|
71
|
+
check(current == 217.5f)
|
|
72
|
+
current = translation.update(current, secondOwner, 64f)
|
|
73
|
+
check(current == 281.5f)
|
|
74
|
+
current = translation.update(current, firstOwner, null)
|
|
75
|
+
check(current == 51.5f)
|
|
76
|
+
current = translation.update(current, secondOwner, null)
|
|
77
|
+
check(current == -12.5f)
|
|
78
|
+
check(translation.isEmpty)
|
|
79
|
+
|
|
80
|
+
// Zero is a real owned contribution; only null releases ownership.
|
|
81
|
+
current = translation.update(current, firstOwner, 0f)
|
|
82
|
+
check(current == -12.5f)
|
|
83
|
+
check(!translation.isEmpty)
|
|
84
|
+
current = translation.update(current, secondOwner, 0f)
|
|
85
|
+
current = translation.update(current, firstOwner, null)
|
|
86
|
+
check(current == -12.5f)
|
|
87
|
+
check(!translation.isEmpty)
|
|
88
|
+
current = translation.update(current, secondOwner, null)
|
|
89
|
+
check(current == -12.5f)
|
|
90
|
+
check(translation.isEmpty)
|
|
91
|
+
current = translation.update(9f, firstOwner, null)
|
|
92
|
+
check(current == 9f)
|
|
93
|
+
check(translation.isEmpty)
|
|
94
|
+
println("NativeScroller refresh translation: repeat, resize, nested release orders, external baseline, zero contribution, and final restoration passed")
|
|
37
95
|
}
|
package/docs/SPEC.md
CHANGED
|
@@ -3,6 +3,65 @@
|
|
|
3
3
|
Existing PagerView and CollapsiblePagerView APIs are documented in README.md.
|
|
4
4
|
The sections below define native coordination boundaries without new React props.
|
|
5
5
|
|
|
6
|
+
## Android NativeScroller refresh indicator placement
|
|
7
|
+
|
|
8
|
+
Status: implemented; Android API 36 Home pull-gesture visibility verified on
|
|
9
|
+
2026-09-28. Spot, Perps and DeFi (RN NativeScroller), plus the unchanged NFT and
|
|
10
|
+
History (NativeList), each pass header-origin and content-origin pulls. Ten
|
|
11
|
+
recordings show a visible indicator below the shared tabs, one refresh event and
|
|
12
|
+
normal disappearance. This verifies UI feedback and event dispatch, not
|
|
13
|
+
downstream request completion. Dynamic caller offset, refresh-time header resize
|
|
14
|
+
and nested-pager replacement remain separate device acceptance work.
|
|
15
|
+
In smooth-header mode, each pager contributes its header-plus-sticky height in
|
|
16
|
+
physical pixels to the RN NativeScroller refresh indicator's `translationY`.
|
|
17
|
+
The coordinator identifies only the refresh layout's direct indicator child;
|
|
18
|
+
it MUST NOT translate the content ScrollView or change refresh offsets, bounds,
|
|
19
|
+
scale, animation, visibility, trigger distance, refreshing state or callbacks.
|
|
20
|
+
RN retains ownership of `progressViewOffset`, including dynamic caller updates.
|
|
21
|
+
NativeList keeps its existing independent refresh-indicator positioning.
|
|
22
|
+
|
|
23
|
+
One state per indicator combines weak owner contributions on the UI thread.
|
|
24
|
+
Repeated application and header resizing replace only the calling owner's
|
|
25
|
+
contribution. An external translation different from the last applied value
|
|
26
|
+
becomes the new baseline. Removing an owner preserves the remaining contributions;
|
|
27
|
+
removing the final owner restores the latest baseline. A zero contribution still
|
|
28
|
+
has an owner until explicitly released. State MUST NOT strongly retain an owner
|
|
29
|
+
or the indicator. Late mounting, indicator replacement and scroller/pager detach
|
|
30
|
+
use the existing native layout lifecycle; there is no JavaScript frame callback.
|
|
31
|
+
|
|
32
|
+
The standalone JVM policy test covers nonzero baselines, repeated application,
|
|
33
|
+
resize, nested owners removed in either order, external translation updates,
|
|
34
|
+
zero contributions and final restoration. It does not prove Android view
|
|
35
|
+
discovery, actual refresh state or rendered placement. Runtime acceptance requires
|
|
36
|
+
header-origin and content-origin pulls on NativeScroller pages, caller offset
|
|
37
|
+
changes, refresh-time layout changes, replacement/detach, and visible disappearance
|
|
38
|
+
after completion; existing NativeList pages must remain unchanged.
|
|
39
|
+
|
|
40
|
+
## iOS refresh indicator placement
|
|
41
|
+
|
|
42
|
+
Status: source implementation; real refresh-cycle acceptance pending.
|
|
43
|
+
In smooth-header mode, the pager shifts the refresh control's sublayers upward
|
|
44
|
+
by its header-plus-sticky height. It does not change the refresh control frame,
|
|
45
|
+
bounds, scroll offset, refresh state or JavaScript callback. RN's caller-owned
|
|
46
|
+
`progressViewOffset` remains untouched, including dynamic updates whose bounds
|
|
47
|
+
origin could otherwise collide with a coordinator-applied value.
|
|
48
|
+
|
|
49
|
+
One internal state per UIRefreshControl combines weak pager-owner contributions.
|
|
50
|
+
It preserves the original sublayer transform and clipping setting. Repeated
|
|
51
|
+
attachment and header resizing replace an owner's contribution. Layer clipping
|
|
52
|
+
is disabled while coordinating so it cannot hide the translated indicator.
|
|
53
|
+
A control replacement, scroll-view release, pager recycling or destruction
|
|
54
|
+
removes only that owner's contribution and restores the baseline when none remain.
|
|
55
|
+
Controls installed after the scroller is attached are also coordinated. Native
|
|
56
|
+
UICollectionView controls and RN ScrollView controls use this same UIKit boundary.
|
|
57
|
+
|
|
58
|
+
Native layer observations reapply the contribution after an external sublayer
|
|
59
|
+
transform change, without observing or interpreting UIKit/RN bounds writes.
|
|
60
|
+
There is no per-frame JavaScript work. Acceptance must cover real UIKit pulling
|
|
61
|
+
and refresh completion, caller offset changes, late installation/replacement,
|
|
62
|
+
retained-page switching, clipping and nested-owner cleanup. Programmatic UIKit
|
|
63
|
+
unit tests do not substitute for real pull-gesture visibility acceptance.
|
|
64
|
+
|
|
6
65
|
## iOS shared-header hit testing
|
|
7
66
|
|
|
8
67
|
Smooth-header hit forwarding is restricted to the pager's own interactive
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
#import "React/RCTScrollViewComponentView.h"
|
|
12
12
|
#import "React/RCTSurfaceTouchHandler.h"
|
|
13
13
|
#import "React/RCTTouchHandler.h"
|
|
14
|
+
#import <objc/runtime.h>
|
|
14
15
|
|
|
15
16
|
// OneKey patch: deduplicate structural mutation parents before scanning the page.
|
|
16
17
|
#include <unordered_set>
|
|
@@ -18,6 +19,116 @@
|
|
|
18
19
|
using namespace facebook::react;
|
|
19
20
|
|
|
20
21
|
static void *RNCCollapsiblePagerContentOffsetContext = &RNCCollapsiblePagerContentOffsetContext;
|
|
22
|
+
static void *RNCRefreshControlLayerContext = &RNCRefreshControlLayerContext;
|
|
23
|
+
static void *RNCScrollRefreshControlContext = &RNCScrollRefreshControlContext;
|
|
24
|
+
static char RNCRefreshControlOffsetKey;
|
|
25
|
+
|
|
26
|
+
@interface RNCRefreshControlOffsetState : NSObject
|
|
27
|
+
+ (void)setOffset:(CGFloat)offset forControl:(UIRefreshControl *)control owner:(id)owner;
|
|
28
|
+
+ (void)removeControl:(UIRefreshControl *)control owner:(id)owner;
|
|
29
|
+
@end
|
|
30
|
+
|
|
31
|
+
@implementation RNCRefreshControlOffsetState {
|
|
32
|
+
__weak UIRefreshControl *_control;
|
|
33
|
+
NSMapTable<id, NSNumber *> *_offsets;
|
|
34
|
+
CATransform3D _baselineTransform;
|
|
35
|
+
CATransform3D _appliedTransform;
|
|
36
|
+
BOOL _originalMasksToBounds;
|
|
37
|
+
BOOL _applying;
|
|
38
|
+
BOOL _observing;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
- (instancetype)initWithControl:(UIRefreshControl *)control
|
|
42
|
+
{
|
|
43
|
+
if ((self = [super init])) {
|
|
44
|
+
_control = control;
|
|
45
|
+
_baselineTransform = control.layer.sublayerTransform;
|
|
46
|
+
_originalMasksToBounds = control.layer.masksToBounds;
|
|
47
|
+
_offsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
48
|
+
[control.layer addObserver:self forKeyPath:@"sublayerTransform"
|
|
49
|
+
options:NSKeyValueObservingOptionNew
|
|
50
|
+
context:RNCRefreshControlLayerContext];
|
|
51
|
+
[control.layer addObserver:self forKeyPath:@"masksToBounds"
|
|
52
|
+
options:NSKeyValueObservingOptionNew
|
|
53
|
+
context:RNCRefreshControlLayerContext];
|
|
54
|
+
_observing = YES;
|
|
55
|
+
}
|
|
56
|
+
return self;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
- (void)apply
|
|
60
|
+
{
|
|
61
|
+
UIRefreshControl *control = _control;
|
|
62
|
+
if (control == nil) return;
|
|
63
|
+
CGFloat offset = 0;
|
|
64
|
+
for (NSNumber *value in _offsets.objectEnumerator) offset += value.doubleValue;
|
|
65
|
+
CATransform3D transform = CATransform3DConcat(
|
|
66
|
+
_baselineTransform, CATransform3DMakeTranslation(0, -offset, 0));
|
|
67
|
+
_appliedTransform = transform;
|
|
68
|
+
_applying = YES;
|
|
69
|
+
// Visual placement must not change UIKit's refresh geometry or RN's bounds
|
|
70
|
+
// origin (progressViewOffset). Allow the shifted indicator outside its host.
|
|
71
|
+
[CATransaction begin];
|
|
72
|
+
[CATransaction setDisableActions:YES];
|
|
73
|
+
control.layer.masksToBounds = _offsets.count == 0 ? _originalMasksToBounds : NO;
|
|
74
|
+
if (!CATransform3DEqualToTransform(control.layer.sublayerTransform, transform)) {
|
|
75
|
+
control.layer.sublayerTransform = transform;
|
|
76
|
+
}
|
|
77
|
+
[CATransaction commit];
|
|
78
|
+
_applying = NO;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
+ (void)setOffset:(CGFloat)offset forControl:(UIRefreshControl *)control owner:(id)owner
|
|
82
|
+
{
|
|
83
|
+
if (control == nil) return;
|
|
84
|
+
RNCRefreshControlOffsetState *state = objc_getAssociatedObject(control, &RNCRefreshControlOffsetKey);
|
|
85
|
+
if (state == nil) {
|
|
86
|
+
state = [[self alloc] initWithControl:control];
|
|
87
|
+
objc_setAssociatedObject(control, &RNCRefreshControlOffsetKey, state, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
88
|
+
}
|
|
89
|
+
[state->_offsets setObject:@(offset) forKey:owner];
|
|
90
|
+
[state apply];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
+ (void)removeControl:(UIRefreshControl *)control owner:(id)owner
|
|
94
|
+
{
|
|
95
|
+
if (control == nil) return;
|
|
96
|
+
RNCRefreshControlOffsetState *state = objc_getAssociatedObject(control, &RNCRefreshControlOffsetKey);
|
|
97
|
+
if (state == nil) return;
|
|
98
|
+
[state->_offsets removeObjectForKey:owner];
|
|
99
|
+
[state apply];
|
|
100
|
+
if (state->_offsets.count == 0) {
|
|
101
|
+
[control.layer removeObserver:state forKeyPath:@"sublayerTransform" context:RNCRefreshControlLayerContext];
|
|
102
|
+
[control.layer removeObserver:state forKeyPath:@"masksToBounds" context:RNCRefreshControlLayerContext];
|
|
103
|
+
state->_observing = NO;
|
|
104
|
+
objc_setAssociatedObject(control, &RNCRefreshControlOffsetKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
|
|
109
|
+
change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context
|
|
110
|
+
{
|
|
111
|
+
if (context == RNCRefreshControlLayerContext) {
|
|
112
|
+
if (_applying) return;
|
|
113
|
+
if ([keyPath isEqualToString:@"sublayerTransform"]) {
|
|
114
|
+
CATransform3D next = [change[NSKeyValueChangeNewKey] CATransform3DValue];
|
|
115
|
+
if (CATransform3DEqualToTransform(next, _appliedTransform)) return;
|
|
116
|
+
_baselineTransform = next;
|
|
117
|
+
}
|
|
118
|
+
[self apply];
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
- (void)dealloc
|
|
125
|
+
{
|
|
126
|
+
if (_observing && _control != nil) {
|
|
127
|
+
[_control.layer removeObserver:self forKeyPath:@"sublayerTransform" context:RNCRefreshControlLayerContext];
|
|
128
|
+
[_control.layer removeObserver:self forKeyPath:@"masksToBounds" context:RNCRefreshControlLayerContext];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
@end
|
|
21
132
|
static NSString *const RNCCollapsiblePagerNativeScrollerIDPrefix =
|
|
22
133
|
@"rnc-collapsible-pager-native-scroller:";
|
|
23
134
|
|
|
@@ -973,6 +1084,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
973
1084
|
- (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)pageIndex;
|
|
974
1085
|
- (void)updateSharedHeaderPressCancellationGesture;
|
|
975
1086
|
- (void)detachSharedHeaderPressCancellationGesture;
|
|
1087
|
+
- (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
1088
|
+
- (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
976
1089
|
- (void)sharedHeaderPressCancellationGestureChanged:(UIPanGestureRecognizer *)recognizer;
|
|
977
1090
|
- (void)outerPagerGestureChanged:(UIPanGestureRecognizer *)recognizer;
|
|
978
1091
|
- (void)verticalPagerGuardGestureChanged:(UIPanGestureRecognizer *)recognizer;
|
|
@@ -1023,6 +1136,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1023
1136
|
NSString *_retainedPages;
|
|
1024
1137
|
NSMutableDictionary<NSString *, NSNumber *> *_pageOffsets;
|
|
1025
1138
|
NSMapTable<UIScrollView *, NSValue *> *_originalInsets;
|
|
1139
|
+
NSMapTable<UIScrollView *, UIRefreshControl *> *_managedRefreshControls;
|
|
1140
|
+
NSHashTable<UIScrollView *> *_refreshControlObservedScrollViews;
|
|
1026
1141
|
NSMapTable<UIScrollView *, NSNumber *> *_appliedTopInsets;
|
|
1027
1142
|
NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
|
|
1028
1143
|
NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
|
|
@@ -1066,6 +1181,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1066
1181
|
_pageOffsets = [NSMutableDictionary new];
|
|
1067
1182
|
_releasedPageScrollStates = [NSMutableDictionary new];
|
|
1068
1183
|
_originalInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1184
|
+
_managedRefreshControls = [NSMapTable weakToWeakObjectsMapTable];
|
|
1185
|
+
_refreshControlObservedScrollViews = [NSHashTable weakObjectsHashTable];
|
|
1069
1186
|
_appliedTopInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1070
1187
|
_originalIndicatorInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1071
1188
|
_originalAlwaysBounceVertical = [NSMapTable weakToStrongObjectsMapTable];
|
|
@@ -1201,6 +1318,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1201
1318
|
|
|
1202
1319
|
- (void)dealloc
|
|
1203
1320
|
{
|
|
1321
|
+
for (UIScrollView *scrollView in _refreshControlObservedScrollViews.allObjects) {
|
|
1322
|
+
[self releaseRefreshControlForScrollView:scrollView];
|
|
1323
|
+
}
|
|
1204
1324
|
[self detachSharedHeaderPressCancellationGesture];
|
|
1205
1325
|
}
|
|
1206
1326
|
|
|
@@ -1739,6 +1859,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1739
1859
|
[self detachSharedHeaderPressCancellationGesture];
|
|
1740
1860
|
[self detachScrollObserver];
|
|
1741
1861
|
[self restoreSharedHeadersToContainer];
|
|
1862
|
+
for (UIScrollView *scrollView in _refreshControlObservedScrollViews.allObjects) {
|
|
1863
|
+
[self releaseRefreshControlForScrollView:scrollView];
|
|
1864
|
+
}
|
|
1742
1865
|
for (UIScrollView *scrollView in _originalInsets.keyEnumerator) {
|
|
1743
1866
|
NSValue *value = [_originalInsets objectForKey:scrollView];
|
|
1744
1867
|
if (value != nil) {
|
|
@@ -2428,10 +2551,44 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2428
2551
|
}
|
|
2429
2552
|
}
|
|
2430
2553
|
|
|
2554
|
+
- (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView
|
|
2555
|
+
{
|
|
2556
|
+
if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled) {
|
|
2557
|
+
[self releaseRefreshControlForScrollView:scrollView];
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (![_refreshControlObservedScrollViews containsObject:scrollView]) {
|
|
2561
|
+
[_refreshControlObservedScrollViews addObject:scrollView];
|
|
2562
|
+
[scrollView addObserver:self forKeyPath:@"refreshControl"
|
|
2563
|
+
options:NSKeyValueObservingOptionNew
|
|
2564
|
+
context:RNCScrollRefreshControlContext];
|
|
2565
|
+
}
|
|
2566
|
+
UIRefreshControl *previous = [_managedRefreshControls objectForKey:scrollView];
|
|
2567
|
+
UIRefreshControl *current = scrollView.refreshControl;
|
|
2568
|
+
if (previous != current) {
|
|
2569
|
+
[RNCRefreshControlOffsetState removeControl:previous owner:self];
|
|
2570
|
+
[_managedRefreshControls removeObjectForKey:scrollView];
|
|
2571
|
+
if (current != nil) [_managedRefreshControls setObject:current forKey:scrollView];
|
|
2572
|
+
}
|
|
2573
|
+
[RNCRefreshControlOffsetState setOffset:_headerHeight + _stickyHeaderHeight
|
|
2574
|
+
forControl:current owner:self];
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
- (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView
|
|
2578
|
+
{
|
|
2579
|
+
if ([_refreshControlObservedScrollViews containsObject:scrollView]) {
|
|
2580
|
+
[scrollView removeObserver:self forKeyPath:@"refreshControl" context:RNCScrollRefreshControlContext];
|
|
2581
|
+
[_refreshControlObservedScrollViews removeObject:scrollView];
|
|
2582
|
+
}
|
|
2583
|
+
[RNCRefreshControlOffsetState removeControl:[_managedRefreshControls objectForKey:scrollView] owner:self];
|
|
2584
|
+
[_managedRefreshControls removeObjectForKey:scrollView];
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2431
2587
|
// OneKey patch: release observers, headers and saved state while the scroll view
|
|
2432
2588
|
// still represents the old list, before Fabric applies its next owner's props.
|
|
2433
2589
|
- (void)restoreScrollViewState:(UIScrollView *)scrollView resetSavedOffset:(BOOL)resetSavedOffset
|
|
2434
2590
|
{
|
|
2591
|
+
[self releaseRefreshControlForScrollView:scrollView];
|
|
2435
2592
|
if (scrollView == _observedScrollView) {
|
|
2436
2593
|
[self detachScrollObserver];
|
|
2437
2594
|
if (resetSavedOffset) _pageOffsets[self.currentPageKey] = @(_headerOffset);
|
|
@@ -2548,6 +2705,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2548
2705
|
// This host already includes the fixed header and safe area in its frame.
|
|
2549
2706
|
// UIKit automatic adjustment would add them a second time to short pages.
|
|
2550
2707
|
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
|
2708
|
+
[self updateRefreshControlForScrollView:scrollView];
|
|
2551
2709
|
CGFloat previousTopInset = scrollView.contentInset.top;
|
|
2552
2710
|
CGFloat previousLogicalOffset = scrollView.contentOffset.y + previousTopInset;
|
|
2553
2711
|
NSNumber *appliedTop = [_appliedTopInsets objectForKey:scrollView];
|
|
@@ -2636,6 +2794,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2636
2794
|
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
|
|
2637
2795
|
context:(void *)context
|
|
2638
2796
|
{
|
|
2797
|
+
if (context == RNCScrollRefreshControlContext) {
|
|
2798
|
+
[self updateRefreshControlForScrollView:(UIScrollView *)object];
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2639
2801
|
if (context == RNCCollapsiblePagerContentOffsetContext && object == _observedScrollView) {
|
|
2640
2802
|
if ([keyPath isEqualToString:@"contentSize"]) {
|
|
2641
2803
|
NSValue *oldSize = change[NSKeyValueChangeOldKey];
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#import <XCTest/XCTest.h>
|
|
2
|
+
#import "RNCCollapsiblePagerViewComponentView.h"
|
|
3
|
+
|
|
4
|
+
@interface RNCCollapsiblePagerViewComponentView (RefreshControlTesting)
|
|
5
|
+
- (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
6
|
+
- (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
7
|
+
@end
|
|
8
|
+
|
|
9
|
+
@interface RNCCollapsiblePagerRefreshControlTests : XCTestCase
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation RNCCollapsiblePagerRefreshControlTests
|
|
13
|
+
|
|
14
|
+
- (RNCCollapsiblePagerViewComponentView *)pagerWithHeader:(CGFloat)height
|
|
15
|
+
{
|
|
16
|
+
RNCCollapsiblePagerViewComponentView *pager = [RNCCollapsiblePagerViewComponentView new];
|
|
17
|
+
[pager setValue:@YES forKey:@"nativeSmoothHeaderScrollEnabled"];
|
|
18
|
+
[pager setValue:@(height) forKey:@"headerHeight"];
|
|
19
|
+
[pager setValue:@44 forKey:@"stickyHeaderHeight"];
|
|
20
|
+
return pager;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (void)testLateControlAndCallerOffsetUpdatesPreserveBaseline
|
|
24
|
+
{
|
|
25
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:64];
|
|
26
|
+
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
|
|
27
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
28
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
29
|
+
control.bounds = CGRectMake(3, -12, 320, 60);
|
|
30
|
+
scroll.refreshControl = control;
|
|
31
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
32
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -12, 0.001);
|
|
33
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
34
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
35
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -12, 0.001);
|
|
36
|
+
control.layer.sublayerTransform = control.layer.sublayerTransform;
|
|
37
|
+
// Also cover an explicit no-op KVO notification, even if CALayer's setter
|
|
38
|
+
// suppresses notifications for equal transforms on the current OS.
|
|
39
|
+
[control.layer willChangeValueForKey:@"sublayerTransform"];
|
|
40
|
+
[control.layer didChangeValueForKey:@"sublayerTransform"];
|
|
41
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
42
|
+
|
|
43
|
+
// Fabric's progressViewOffset setter replaces bounds.origin.y directly.
|
|
44
|
+
control.bounds = CGRectMake(3, -20, 320, 60);
|
|
45
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
46
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -20, 0.001);
|
|
47
|
+
[pager setValue:@100 forKey:@"headerHeight"];
|
|
48
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
49
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -144, 0.001);
|
|
50
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -20, 0.001);
|
|
51
|
+
[pager releaseRefreshControlForScrollView:scroll];
|
|
52
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -20, 0.001);
|
|
53
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.x, 3, 0.001);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
- (void)testUIKitSizeChangesAndRefreshCycleDoNotAccumulateOffset
|
|
57
|
+
{
|
|
58
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:64];
|
|
59
|
+
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
|
|
60
|
+
// UIKit ignores offscreen beginRefreshing calls. Exercise the actual refresh
|
|
61
|
+
// lifecycle in a visible window rather than weakening the state assertion.
|
|
62
|
+
UIWindow *window = [[UIWindow alloc] initWithFrame:scroll.frame];
|
|
63
|
+
UIViewController *controller = [UIViewController new];
|
|
64
|
+
window.rootViewController = controller;
|
|
65
|
+
scroll.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
|
66
|
+
[controller.view addSubview:scroll];
|
|
67
|
+
window.hidden = NO;
|
|
68
|
+
[self addTeardownBlock:^{ window.hidden = YES; }];
|
|
69
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
70
|
+
scroll.refreshControl = control;
|
|
71
|
+
[window layoutIfNeeded];
|
|
72
|
+
[scroll layoutIfNeeded];
|
|
73
|
+
XCTAssertEqual(control.window, window);
|
|
74
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
75
|
+
CGRect bounds = control.bounds;
|
|
76
|
+
bounds.size = CGSizeMake(320, 60);
|
|
77
|
+
control.bounds = bounds;
|
|
78
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
79
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
80
|
+
[control beginRefreshing];
|
|
81
|
+
[control layoutIfNeeded];
|
|
82
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
83
|
+
XCTAssertTrue(control.isRefreshing);
|
|
84
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
85
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
86
|
+
[control endRefreshing];
|
|
87
|
+
[control layoutIfNeeded];
|
|
88
|
+
XCTAssertFalse(control.isRefreshing);
|
|
89
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
90
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
91
|
+
[pager releaseRefreshControlForScrollView:scroll];
|
|
92
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
93
|
+
XCTAssertTrue(CATransform3DIsIdentity(control.layer.sublayerTransform));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
- (void)testReplacementAndNestedOwnerReleaseRestoreOnlyTheirContributions
|
|
97
|
+
{
|
|
98
|
+
RNCCollapsiblePagerViewComponentView *first = [self pagerWithHeader:64];
|
|
99
|
+
RNCCollapsiblePagerViewComponentView *second = [self pagerWithHeader:20];
|
|
100
|
+
UICollectionView *scroll = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)
|
|
101
|
+
collectionViewLayout:[UICollectionViewFlowLayout new]];
|
|
102
|
+
UIRefreshControl *original = [UIRefreshControl new];
|
|
103
|
+
scroll.refreshControl = original;
|
|
104
|
+
[first updateRefreshControlForScrollView:scroll];
|
|
105
|
+
[second updateRefreshControlForScrollView:scroll];
|
|
106
|
+
XCTAssertEqualWithAccuracy(original.layer.sublayerTransform.m42, -172, 0.001);
|
|
107
|
+
[first releaseRefreshControlForScrollView:scroll];
|
|
108
|
+
XCTAssertEqualWithAccuracy(original.layer.sublayerTransform.m42, -64, 0.001);
|
|
109
|
+
|
|
110
|
+
UIRefreshControl *replacement = [UIRefreshControl new];
|
|
111
|
+
replacement.bounds = CGRectMake(0, -8, 320, 60);
|
|
112
|
+
scroll.refreshControl = replacement;
|
|
113
|
+
XCTAssertTrue(CATransform3DIsIdentity(original.layer.sublayerTransform));
|
|
114
|
+
XCTAssertEqualWithAccuracy(replacement.layer.sublayerTransform.m42, -64, 0.001);
|
|
115
|
+
XCTAssertEqualWithAccuracy(replacement.bounds.origin.y, -8, 0.001);
|
|
116
|
+
[second prepareForRecycle];
|
|
117
|
+
XCTAssertEqualWithAccuracy(replacement.bounds.origin.y, -8, 0.001);
|
|
118
|
+
XCTAssertTrue(CATransform3DIsIdentity(replacement.layer.sublayerTransform));
|
|
119
|
+
scroll.refreshControl = nil;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
- (void)testPagerDestructionRestoresControl
|
|
123
|
+
{
|
|
124
|
+
UIScrollView *scroll = [UIScrollView new];
|
|
125
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
126
|
+
scroll.refreshControl = control;
|
|
127
|
+
@autoreleasepool {
|
|
128
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:64];
|
|
129
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
130
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -108, 0.001);
|
|
131
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
132
|
+
}
|
|
133
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, 0, 0.001);
|
|
134
|
+
XCTAssertTrue(CATransform3DIsIdentity(control.layer.sublayerTransform));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
- (void)testCallerOffsetCollisionDoesNotAffectVisualContribution
|
|
138
|
+
{
|
|
139
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:56];
|
|
140
|
+
UIScrollView *scroll = [UIScrollView new];
|
|
141
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
142
|
+
scroll.refreshControl = control;
|
|
143
|
+
control.bounds = CGRectMake(0, -200, 320, 60);
|
|
144
|
+
[pager updateRefreshControlForScrollView:scroll];
|
|
145
|
+
control.bounds = CGRectMake(0, -100, 320, 60);
|
|
146
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -100, 0.001);
|
|
147
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, -100, 0.001);
|
|
148
|
+
[pager releaseRefreshControlForScrollView:scroll];
|
|
149
|
+
XCTAssertEqualWithAccuracy(control.bounds.origin.y, -100, 0.001);
|
|
150
|
+
XCTAssertTrue(CATransform3DIsIdentity(control.layer.sublayerTransform));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
- (void)testNestedOwnerDestructionPreservesBaselineAndRestoresClipping
|
|
154
|
+
{
|
|
155
|
+
RNCCollapsiblePagerViewComponentView *second = [self pagerWithHeader:20];
|
|
156
|
+
UIScrollView *scroll = [UIScrollView new];
|
|
157
|
+
UIRefreshControl *control = [UIRefreshControl new];
|
|
158
|
+
scroll.refreshControl = control;
|
|
159
|
+
CATransform3D baseline = CATransform3DMakeTranslation(7, 13, 0);
|
|
160
|
+
control.layer.sublayerTransform = baseline;
|
|
161
|
+
control.layer.masksToBounds = YES;
|
|
162
|
+
@autoreleasepool {
|
|
163
|
+
RNCCollapsiblePagerViewComponentView *first = [self pagerWithHeader:64];
|
|
164
|
+
[first updateRefreshControlForScrollView:scroll];
|
|
165
|
+
[second updateRefreshControlForScrollView:scroll];
|
|
166
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, 13 - 172, 0.001);
|
|
167
|
+
XCTAssertFalse(control.layer.masksToBounds);
|
|
168
|
+
}
|
|
169
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, 13 - 64, 0.001);
|
|
170
|
+
// A UIKit layer reset must not discard the remaining visual contribution.
|
|
171
|
+
control.layer.sublayerTransform = baseline;
|
|
172
|
+
control.layer.masksToBounds = YES;
|
|
173
|
+
XCTAssertEqualWithAccuracy(control.layer.sublayerTransform.m42, 13 - 64, 0.001);
|
|
174
|
+
XCTAssertFalse(control.layer.masksToBounds);
|
|
175
|
+
[second releaseRefreshControlForScrollView:scroll];
|
|
176
|
+
XCTAssertTrue(CATransform3DEqualToTransform(control.layer.sublayerTransform, baseline));
|
|
177
|
+
XCTAssertTrue(control.layer.masksToBounds);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@end
|
package/package.json
CHANGED