react-native-tvos 0.83.3-0 → 0.83.4-0
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/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js +1 -0
- package/Libraries/Components/ScrollView/ScrollView.d.ts +2 -1
- package/Libraries/Components/ScrollView/ScrollView.js +8 -1
- package/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +2 -0
- package/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +2 -1
- package/Libraries/Components/TV/TVViewPropTypes.js +7 -0
- package/Libraries/Components/View/View.js +6 -0
- package/Libraries/Core/ReactNativeVersion.js +1 -1
- package/Libraries/NativeComponent/TVViewConfig.js +1 -0
- package/Libraries/Pressability/Pressability.js +7 -0
- package/React/Base/RCTVersion.m +1 -1
- package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h +1 -0
- package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm +3 -0
- package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +110 -4
- package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +2 -0
- package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +17 -0
- package/ReactAndroid/gradle.properties +1 -1
- package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
- package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +49 -5
- package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt +7 -0
- package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +46 -4
- package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt +69 -0
- package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt +7 -0
- package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +1 -0
- package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +4 -0
- package/ReactCommon/cxxreact/ReactNativeVersion.h +2 -2
- package/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp +10 -0
- package/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h +1 -0
- package/ReactCommon/react/renderer/components/scrollview/conversions.h +6 -0
- package/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp +18 -1
- package/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h +2 -0
- package/ReactCommon/react/renderer/components/scrollview/primitives.h +1 -1
- package/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +11 -1
- package/ReactCommon/react/renderer/components/view/BaseViewProps.h +4 -0
- package/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp +18 -0
- package/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.h +1 -0
- package/package.json +8 -8
- package/types/public/ReactNativeTVTypes.d.ts +19 -2
|
@@ -515,8 +515,9 @@ export interface ScrollViewPropsIOS {
|
|
|
515
515
|
* - `start` (the default) will align the snap at the left (horizontal) or top (vertical)
|
|
516
516
|
* - `center` will align the snap in the center
|
|
517
517
|
* - `end` will align the snap at the right (horizontal) or bottom (vertical)
|
|
518
|
+
* - `item` will align the snap according to the value of `scrollSnapAlign` for individual items in the scroll view (TV platforms only).
|
|
518
519
|
*/
|
|
519
|
-
snapToAlignment?: 'start' | 'center' | 'end' | undefined;
|
|
520
|
+
snapToAlignment?: 'start' | 'center' | 'end' | 'item' | undefined;
|
|
520
521
|
|
|
521
522
|
/**
|
|
522
523
|
* Fires when the scroll view scrolls to top after the status bar has been tapped
|
|
@@ -612,8 +612,15 @@ type ScrollViewBaseProps = $ReadOnly<{
|
|
|
612
612
|
* - `'start'` (the default) will align the snap at the left (horizontal) or top (vertical)
|
|
613
613
|
* - `'center'` will align the snap in the center
|
|
614
614
|
* - `'end'` will align the snap at the right (horizontal) or bottom (vertical)
|
|
615
|
+
* - `'item'` will align the snap according to the value of `scrollSnapAlign` for individual items in the scroll view (TV platforms only).
|
|
615
616
|
*/
|
|
616
|
-
snapToAlignment?: ?('start' | 'center' | 'end'),
|
|
617
|
+
snapToAlignment?: ?('start' | 'center' | 'end' | 'item'),
|
|
618
|
+
/**
|
|
619
|
+
* Padding applied when snapping to items using `snapToAlignment="item"`.
|
|
620
|
+
* This is set on the parent scroll view, not directly on child items.
|
|
621
|
+
* Only used when `snapToAlignment` is set to `'item'`.
|
|
622
|
+
*/
|
|
623
|
+
snapToItemPadding?: ?number,
|
|
617
624
|
/**
|
|
618
625
|
* When set, causes the scroll view to stop at multiples of the value of
|
|
619
626
|
* `snapToInterval`. This can be used for paginating through children
|
|
@@ -86,6 +86,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
|
|
|
86
86
|
borderLeftColor: {
|
|
87
87
|
process: require('../../StyleSheet/processColor').default,
|
|
88
88
|
},
|
|
89
|
+
snapToItemPadding: true,
|
|
89
90
|
pointerEvents: true,
|
|
90
91
|
isInvertedVirtualizedList: true,
|
|
91
92
|
},
|
|
@@ -152,6 +153,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
|
|
|
152
153
|
showsHorizontalScrollIndicator: true,
|
|
153
154
|
showsVerticalScrollIndicator: true,
|
|
154
155
|
showsScrollIndex: true,
|
|
156
|
+
snapToItemPadding: true,
|
|
155
157
|
snapToAlignment: true,
|
|
156
158
|
snapToEnd: true,
|
|
157
159
|
snapToInterval: true,
|
|
@@ -69,11 +69,12 @@ export type ScrollViewNativeProps = $ReadOnly<{
|
|
|
69
69
|
scrollPerfTag?: ?string,
|
|
70
70
|
scrollToOverflowEnabled?: ?boolean,
|
|
71
71
|
scrollsToTop?: ?boolean,
|
|
72
|
+
snapToItemPadding?: ?number,
|
|
72
73
|
sendMomentumEvents?: ?boolean,
|
|
73
74
|
showsHorizontalScrollIndicator?: ?boolean,
|
|
74
75
|
showsScrollIndex?: ?boolean,
|
|
75
76
|
showsVerticalScrollIndicator?: ?boolean,
|
|
76
|
-
snapToAlignment?: ?('start' | 'center' | 'end'),
|
|
77
|
+
snapToAlignment?: ?('start' | 'center' | 'end' | 'item'),
|
|
77
78
|
snapToEnd?: ?boolean,
|
|
78
79
|
snapToInterval?: ?number,
|
|
79
80
|
snapToOffsets?: ?$ReadOnlyArray<number>,
|
|
@@ -153,6 +153,12 @@ component View(
|
|
|
153
153
|
delete processedProps.isTVSelectable;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// Views with scrollSnapAlign must not be flattened by Fabric, otherwise
|
|
157
|
+
// the prop never reaches the native view and scroll snapping breaks.
|
|
158
|
+
if (processedProps.scrollSnapAlign != null) {
|
|
159
|
+
processedProps.collapsable = false;
|
|
160
|
+
}
|
|
161
|
+
|
|
156
162
|
const actualView =
|
|
157
163
|
ref == null ? (
|
|
158
164
|
<ViewNativeComponent {...processedProps} />
|
|
@@ -398,6 +398,7 @@ export default class Pressability {
|
|
|
398
398
|
_touchActivateTime: ?number;
|
|
399
399
|
_touchState: TouchState = 'NOT_RESPONDER';
|
|
400
400
|
_longPressSent: boolean = false;
|
|
401
|
+
_tvPressInReceived: boolean = false;
|
|
401
402
|
|
|
402
403
|
constructor(config: PressabilityConfig) {
|
|
403
404
|
this.configure(config);
|
|
@@ -416,6 +417,7 @@ export default class Pressability {
|
|
|
416
417
|
this._cancelLongPressDelayTimeout();
|
|
417
418
|
this._cancelPressDelayTimeout();
|
|
418
419
|
this._cancelPressOutDelayTimeout();
|
|
420
|
+
this._tvPressInReceived = false;
|
|
419
421
|
|
|
420
422
|
// Ensure that, if any async event handlers are fired after unmount
|
|
421
423
|
// due to a race, we don't call any configured callbacks.
|
|
@@ -443,6 +445,7 @@ export default class Pressability {
|
|
|
443
445
|
return;
|
|
444
446
|
}
|
|
445
447
|
|
|
448
|
+
this._tvPressInReceived = true;
|
|
446
449
|
this._longPressSent = false;
|
|
447
450
|
|
|
448
451
|
const {onPressIn, onLongPress} = this._config;
|
|
@@ -463,6 +466,10 @@ export default class Pressability {
|
|
|
463
466
|
if (this._config.disabled === true) {
|
|
464
467
|
return;
|
|
465
468
|
}
|
|
469
|
+
if (!this._tvPressInReceived) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this._tvPressInReceived = false;
|
|
466
473
|
this._cancelLongPressDelayTimeout();
|
|
467
474
|
const {onPress, onLongPress, onPressOut, android_disableSound} =
|
|
468
475
|
this._config;
|
package/React/Base/RCTVersion.m
CHANGED
|
@@ -51,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
51
51
|
@property (nonatomic, assign) BOOL snapToStart;
|
|
52
52
|
@property (nonatomic, assign) BOOL snapToEnd;
|
|
53
53
|
@property (nonatomic, copy) NSArray<NSNumber *> *snapToOffsets;
|
|
54
|
+
@property (nonatomic, assign) BOOL scrollSnapEnabled;
|
|
54
55
|
|
|
55
56
|
/*
|
|
56
57
|
* Makes `setContentOffset:` method no-op when given `block` is executed.
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
#import <React/RCTTVRemoteHandler.h>
|
|
31
31
|
#import <React/RCTTVNavigationEventNotification.h>
|
|
32
32
|
#import "React/RCTI18nUtil.h"
|
|
33
|
+
#import "RCTViewComponentView.h"
|
|
33
34
|
#endif
|
|
34
35
|
|
|
35
36
|
using namespace facebook::react;
|
|
@@ -40,6 +41,7 @@ static NSString *kOnScrollEndEvent = @"onScrollEnded";
|
|
|
40
41
|
|
|
41
42
|
static const CGFloat kClippingLeeway = 44.0;
|
|
42
43
|
static const float TV_DEFAULT_SWIPE_DURATION = 0.3;
|
|
44
|
+
static const CGPoint NO_PREFERRED_CONTENT_OFFSET = CGPointMake(CGFLOAT_MIN, CGFLOAT_MIN);
|
|
43
45
|
|
|
44
46
|
static UIScrollViewKeyboardDismissMode RCTUIKeyboardDismissModeFromProps(const ScrollViewProps &props)
|
|
45
47
|
{
|
|
@@ -97,6 +99,8 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
|
|
|
97
99
|
RCTScrollableProtocol,
|
|
98
100
|
RCTEnhancedScrollViewOverridingDelegate>
|
|
99
101
|
|
|
102
|
+
@property (nonatomic, assign) CGPoint preferredContentOffset;
|
|
103
|
+
|
|
100
104
|
@end
|
|
101
105
|
|
|
102
106
|
@implementation RCTScrollViewComponentView {
|
|
@@ -172,6 +176,7 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
|
|
|
172
176
|
_endDraggingSensitivityMultiplier = 1;
|
|
173
177
|
|
|
174
178
|
_tvRemoteGestureRecognizers = [NSMutableDictionary new];
|
|
179
|
+
_preferredContentOffset = NO_PREFERRED_CONTENT_OFFSET;
|
|
175
180
|
}
|
|
176
181
|
|
|
177
182
|
return self;
|
|
@@ -465,6 +470,12 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
|
|
|
465
470
|
scrollView.keyboardDismissMode = RCTUIKeyboardDismissModeFromProps(newScrollViewProps);
|
|
466
471
|
}
|
|
467
472
|
|
|
473
|
+
#if TARGET_OS_TV
|
|
474
|
+
if (oldScrollViewProps.snapToAlignment != newScrollViewProps.snapToAlignment) {
|
|
475
|
+
scrollView.scrollSnapEnabled = newScrollViewProps.snapToAlignment == ScrollViewSnapToAlignment::Item;
|
|
476
|
+
}
|
|
477
|
+
#endif
|
|
478
|
+
|
|
468
479
|
[super updateProps:props oldProps:oldProps];
|
|
469
480
|
}
|
|
470
481
|
|
|
@@ -722,7 +733,10 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
|
|
|
722
733
|
withVelocity:(CGPoint)velocity
|
|
723
734
|
targetContentOffset:(inout CGPoint *)targetContentOffset
|
|
724
735
|
{
|
|
725
|
-
|
|
736
|
+
if (!CGPointEqualToPoint(self.preferredContentOffset, NO_PREFERRED_CONTENT_OFFSET))
|
|
737
|
+
{
|
|
738
|
+
*targetContentOffset = self.preferredContentOffset;
|
|
739
|
+
} else if (fabs(_endDraggingSensitivityMultiplier - 1) > 0.0001f) {
|
|
726
740
|
if (targetContentOffset->y > 0) {
|
|
727
741
|
const CGFloat travel = targetContentOffset->y - scrollView.contentOffset.y;
|
|
728
742
|
targetContentOffset->y = scrollView.contentOffset.y + travel * _endDraggingSensitivityMultiplier;
|
|
@@ -1137,19 +1151,109 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
|
|
|
1137
1151
|
#pragma mark Apple TV swipe and focus handling
|
|
1138
1152
|
|
|
1139
1153
|
#if TARGET_OS_TV
|
|
1154
|
+
// Focus marker helper: traverses view hierarchy to find scrollSnapAlign prop
|
|
1155
|
+
// Returns the view that has the property via the output parameter
|
|
1156
|
+
- (NSString *)findScrollSnapAlignInView:(UIView *)view foundView:(UIView **)outView
|
|
1157
|
+
{
|
|
1158
|
+
UIView *testView = view;
|
|
1159
|
+
UIView *snapTarget;
|
|
1160
|
+
NSString *marker;
|
|
1161
|
+
|
|
1162
|
+
while (testView && testView != self) {
|
|
1163
|
+
if (![testView isKindOfClass:RCTViewComponentView.class])
|
|
1164
|
+
{
|
|
1165
|
+
testView = [testView superview];
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
RCTViewComponentView *componentView = (RCTViewComponentView *)testView;
|
|
1169
|
+
|
|
1170
|
+
const auto &viewProps = static_cast<const facebook::react::BaseViewProps &>(*componentView.props);
|
|
1171
|
+
if (viewProps.scrollSnapAlign.has_value() && !viewProps.scrollSnapAlign.value().empty()) {
|
|
1172
|
+
marker = [NSString stringWithUTF8String:viewProps.scrollSnapAlign.value().c_str()];
|
|
1173
|
+
snapTarget = componentView;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
testView = [testView superview];
|
|
1177
|
+
}
|
|
1178
|
+
*outView = snapTarget;
|
|
1179
|
+
return marker;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
- (void)_handleScrollSnapForFocusedView:(UIView *)focusedView
|
|
1183
|
+
{
|
|
1184
|
+
const auto &scrollProps = static_cast<const ScrollViewProps &>(*_props);
|
|
1185
|
+
UIView *snapAlignView = nil;
|
|
1186
|
+
NSString *scrollSnapAlign = [self findScrollSnapAlignInView:focusedView foundView:&snapAlignView];
|
|
1187
|
+
if (scrollSnapAlign == nil || snapAlignView == nil) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
RCTEnhancedScrollView *scrollView = (RCTEnhancedScrollView *)_scrollView;
|
|
1192
|
+
CGRect focusedFrame = [snapAlignView convertRect:snapAlignView.bounds toView:_scrollView];
|
|
1193
|
+
CGFloat targetOffset;
|
|
1194
|
+
CGFloat snapToItemPadding = scrollProps.snapToItemPadding;
|
|
1195
|
+
|
|
1196
|
+
BOOL isHorizontalSnap = _scrollView.contentSize.width > self.frame.size.width;
|
|
1197
|
+
// Determine axis-specific properties
|
|
1198
|
+
CGFloat viewportSize, focusedOrigin, focusedSize, currentOffset, maxContentSize;
|
|
1199
|
+
if (isHorizontalSnap) {
|
|
1200
|
+
viewportSize = scrollView.bounds.size.width;
|
|
1201
|
+
focusedOrigin = focusedFrame.origin.x;
|
|
1202
|
+
focusedSize = focusedFrame.size.width;
|
|
1203
|
+
currentOffset = scrollView.contentOffset.x;
|
|
1204
|
+
maxContentSize = scrollView.contentSize.width;
|
|
1205
|
+
} else {
|
|
1206
|
+
viewportSize = scrollView.bounds.size.height;
|
|
1207
|
+
focusedOrigin = focusedFrame.origin.y;
|
|
1208
|
+
focusedSize = focusedFrame.size.height;
|
|
1209
|
+
currentOffset = scrollView.contentOffset.y;
|
|
1210
|
+
maxContentSize = scrollView.contentSize.height;
|
|
1211
|
+
}
|
|
1212
|
+
// Calculate target offset based on scrollSnapAlign (unified for both axes)
|
|
1213
|
+
if ([scrollSnapAlign isEqualToString:@"start"]) {
|
|
1214
|
+
targetOffset = focusedOrigin - snapToItemPadding;
|
|
1215
|
+
} else if ([scrollSnapAlign isEqualToString:@"center"]) {
|
|
1216
|
+
CGFloat viewportCenter = viewportSize / 2;
|
|
1217
|
+
CGFloat focusedCenter = focusedOrigin + (focusedSize / 2);
|
|
1218
|
+
targetOffset = focusedCenter - viewportCenter + (snapToItemPadding / 2);
|
|
1219
|
+
} else if ([scrollSnapAlign isEqualToString:@"end"]) {
|
|
1220
|
+
targetOffset = (focusedOrigin + focusedSize) - viewportSize + snapToItemPadding;
|
|
1221
|
+
} else {
|
|
1222
|
+
targetOffset = currentOffset;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Apply snap-to-interval if configured
|
|
1226
|
+
if (scrollView.snapToInterval > 0) {
|
|
1227
|
+
CGFloat interval = scrollView.snapToInterval;
|
|
1228
|
+
targetOffset = floor(targetOffset / interval) * interval;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Clamp to valid range
|
|
1232
|
+
CGFloat maxOffset = MAX(maxContentSize - viewportSize, 0);
|
|
1233
|
+
targetOffset = MAX(0, MIN(targetOffset, maxOffset));
|
|
1234
|
+
CGPoint targetContentOffset = isHorizontalSnap
|
|
1235
|
+
? CGPointMake(targetOffset, scrollView.contentOffset.y)
|
|
1236
|
+
: CGPointMake(scrollView.contentOffset.x, targetOffset);
|
|
1237
|
+
self.preferredContentOffset = targetContentOffset;
|
|
1238
|
+
[_scrollView setContentOffset:targetContentOffset animated:YES];
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1140
1241
|
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context
|
|
1141
1242
|
withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
|
|
1142
1243
|
{
|
|
1143
|
-
|
|
1244
|
+
self.preferredContentOffset = NO_PREFERRED_CONTENT_OFFSET;
|
|
1245
|
+
const auto &scrollProps = static_cast<const ScrollViewProps &>(*_props);
|
|
1246
|
+
BOOL hasItemSnapAlignment = scrollProps.snapToAlignment == ScrollViewSnapToAlignment::Item;
|
|
1247
|
+
if (context.previouslyFocusedView == context.nextFocusedView || (!_props->isTVSelectable && !hasItemSnapAlignment)) {
|
|
1144
1248
|
return;
|
|
1145
1249
|
}
|
|
1146
|
-
if (context.nextFocusedView == self) {
|
|
1250
|
+
if (_props->isTVSelectable && context.nextFocusedView == self) {
|
|
1147
1251
|
[self becomeFirstResponder];
|
|
1148
1252
|
[self addSwipeGestureRecognizers];
|
|
1149
1253
|
// if we enter the scroll view from different view then block first touch event since it is the event that triggered the focus
|
|
1150
1254
|
_blockFirstTouch = (unsigned long)context.focusHeading != 0;
|
|
1151
1255
|
[self addArrowsListeners];
|
|
1152
|
-
} else if (context.previouslyFocusedView == self) {
|
|
1256
|
+
} else if (_props->isTVSelectable && context.previouslyFocusedView == self) {
|
|
1153
1257
|
[self removeArrowsListeners];
|
|
1154
1258
|
[self removeSwipeGestureRecognizers];
|
|
1155
1259
|
[self resignFirstResponder];
|
|
@@ -1171,6 +1275,8 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
|
|
|
1171
1275
|
[self scrollToHorizontalOffset:scrollView.contentSize.width];
|
|
1172
1276
|
}
|
|
1173
1277
|
}
|
|
1278
|
+
} else if ([context.nextFocusedView isDescendantOfView:_scrollView]) {
|
|
1279
|
+
[self _handleScrollSnapForFocusedView:context.nextFocusedView];
|
|
1174
1280
|
}
|
|
1175
1281
|
}
|
|
1176
1282
|
|
|
@@ -80,6 +80,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
80
80
|
@property(nonatomic, nullable) UIFocusGuide *focusGuideLeft;
|
|
81
81
|
@property(nonatomic, nullable) UIFocusGuide *focusGuideRight;
|
|
82
82
|
@property(nonatomic, nullable, strong) RCTTVRemoteSelectHandler *tvRemoteSelectHandler;
|
|
83
|
+
|
|
84
|
+
- (NSString *)scrollSnapAlign;
|
|
83
85
|
#endif
|
|
84
86
|
|
|
85
87
|
/**
|
|
@@ -87,6 +87,7 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
|
|
|
87
87
|
BOOL _trapFocusRight;
|
|
88
88
|
NSArray* _focusDestinations;
|
|
89
89
|
id<UIFocusItem> _previouslyFocusedItem;
|
|
90
|
+
NSString *_scrollSnapAlign;
|
|
90
91
|
RCTSwiftUIContainerViewWrapper *_swiftUIWrapper;
|
|
91
92
|
BOOL _shouldFocusOnMount;
|
|
92
93
|
}
|
|
@@ -441,6 +442,13 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
|
|
|
441
442
|
_motionEffectsAdded = NO;
|
|
442
443
|
}
|
|
443
444
|
|
|
445
|
+
- (NSString *)scrollSnapAlign
|
|
446
|
+
{
|
|
447
|
+
#if TARGET_OS_TV
|
|
448
|
+
return _scrollSnapAlign;
|
|
449
|
+
#endif
|
|
450
|
+
return nil;
|
|
451
|
+
}
|
|
444
452
|
|
|
445
453
|
- (BOOL)isTVFocusGuide
|
|
446
454
|
{
|
|
@@ -1170,6 +1178,15 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
|
|
|
1170
1178
|
_trapFocusDown = newViewProps.trapFocusDown;
|
|
1171
1179
|
_trapFocusLeft = newViewProps.trapFocusLeft;
|
|
1172
1180
|
_trapFocusRight = newViewProps.trapFocusRight;
|
|
1181
|
+
|
|
1182
|
+
// `scrollSnapAlign`
|
|
1183
|
+
if (oldViewProps.scrollSnapAlign != newViewProps.scrollSnapAlign) {
|
|
1184
|
+
if (newViewProps.scrollSnapAlign.has_value()) {
|
|
1185
|
+
_scrollSnapAlign = [[NSString alloc] initWithUTF8String:newViewProps.scrollSnapAlign.value().c_str()];
|
|
1186
|
+
} else {
|
|
1187
|
+
_scrollSnapAlign = nil;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1173
1190
|
#endif
|
|
1174
1191
|
|
|
1175
1192
|
_needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer;
|
package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java
CHANGED
|
@@ -10,6 +10,7 @@ package com.facebook.react.views.scroll;
|
|
|
10
10
|
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_CENTER;
|
|
11
11
|
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISABLED;
|
|
12
12
|
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END;
|
|
13
|
+
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_ITEM;
|
|
13
14
|
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START;
|
|
14
15
|
import static com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView;
|
|
15
16
|
|
|
@@ -134,6 +135,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
|
|
134
135
|
private @Nullable MaintainVisibleScrollPositionHelper mMaintainVisibleContentPositionHelper;
|
|
135
136
|
private int mFadingEdgeLengthStart = 0;
|
|
136
137
|
private int mFadingEdgeLengthEnd = 0;
|
|
138
|
+
private int mSnapToItemPadding;
|
|
137
139
|
|
|
138
140
|
public ReactHorizontalScrollView(Context context) {
|
|
139
141
|
this(context, null);
|
|
@@ -382,6 +384,10 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
|
|
382
384
|
invalidate();
|
|
383
385
|
}
|
|
384
386
|
|
|
387
|
+
public void setSnapToItemPadding(int snapToItemPadding) {
|
|
388
|
+
mSnapToItemPadding = snapToItemPadding;
|
|
389
|
+
}
|
|
390
|
+
|
|
385
391
|
@Override
|
|
386
392
|
protected float getLeftFadingEdgeStrength() {
|
|
387
393
|
float max = Math.max(mFadingEdgeLengthStart, mFadingEdgeLengthEnd);
|
|
@@ -538,6 +544,40 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
|
|
538
544
|
}
|
|
539
545
|
}
|
|
540
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign.
|
|
549
|
+
* Returns true if snap scrolling was performed, false otherwise.
|
|
550
|
+
*/
|
|
551
|
+
private boolean tryScrollSnapToChild(View focused) {
|
|
552
|
+
if (mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
kotlin.Pair<View, String> result = ReactScrollViewHelper.findScrollSnapAlign(focused, this);
|
|
557
|
+
if (result == null) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
View snapTarget = result.getFirst();
|
|
562
|
+
String alignment = result.getSecond();
|
|
563
|
+
|
|
564
|
+
Rect rect = new Rect();
|
|
565
|
+
snapTarget.getDrawingRect(rect);
|
|
566
|
+
offsetDescendantRectToMyCoords(snapTarget, rect);
|
|
567
|
+
|
|
568
|
+
int viewportWidth = getWidth() - getPaddingLeft() - getPaddingRight();
|
|
569
|
+
int maxScrollX = Math.max(0, computeHorizontalScrollRange() - getWidth());
|
|
570
|
+
|
|
571
|
+
Integer targetOffset = ReactScrollViewHelper.computeScrollSnapOffset(
|
|
572
|
+
rect.left, rect.right, viewportWidth, alignment, mSnapInterval, mSnapToItemPadding, maxScrollX);
|
|
573
|
+
if (targetOffset == null) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
reactSmoothScrollTo(targetOffset, getScrollY());
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
|
|
541
581
|
/**
|
|
542
582
|
* Since ReactHorizontalScrollView handles layout changes on JS side, it does not call
|
|
543
583
|
* super.onlayout due to which mIsLayoutDirty flag in HorizontalScrollView remains true and
|
|
@@ -547,8 +587,12 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
|
|
547
587
|
*/
|
|
548
588
|
@Override
|
|
549
589
|
public void requestChildFocus(View child, View focused) {
|
|
550
|
-
if (focused != null
|
|
551
|
-
|
|
590
|
+
if (focused != null) {
|
|
591
|
+
if (!tryScrollSnapToChild(focused)) {
|
|
592
|
+
if (!mPagingEnabled) {
|
|
593
|
+
scrollToChild(focused);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
552
596
|
}
|
|
553
597
|
requestChildFocusWithoutScroll(child, focused);
|
|
554
598
|
}
|
|
@@ -832,7 +876,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
|
|
832
876
|
&& (mPagingEnabled
|
|
833
877
|
|| mSnapInterval != 0
|
|
834
878
|
|| mSnapOffsets != null
|
|
835
|
-
|| mSnapToAlignment != SNAP_ALIGNMENT_DISABLED)) {
|
|
879
|
+
|| (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED && mSnapToAlignment != SNAP_ALIGNMENT_ITEM))) {
|
|
836
880
|
// Cancel any pending runnable and reschedule
|
|
837
881
|
if (mPostTouchRunnable != null) {
|
|
838
882
|
removeCallbacks(mPostTouchRunnable);
|
|
@@ -1280,7 +1324,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
|
|
1280
1324
|
}
|
|
1281
1325
|
|
|
1282
1326
|
// pagingEnabled only allows snapping one interval at a time
|
|
1283
|
-
if (mSnapInterval == 0 && mSnapOffsets == null && mSnapToAlignment == SNAP_ALIGNMENT_DISABLED) {
|
|
1327
|
+
if (mSnapInterval == 0 && mSnapOffsets == null && (mSnapToAlignment == SNAP_ALIGNMENT_DISABLED || mSnapToAlignment == SNAP_ALIGNMENT_ITEM)) {
|
|
1284
1328
|
smoothScrollAndSnap(velocityX);
|
|
1285
1329
|
return;
|
|
1286
1330
|
}
|
|
@@ -1324,7 +1368,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
|
|
|
1324
1368
|
}
|
|
1325
1369
|
}
|
|
1326
1370
|
}
|
|
1327
|
-
} else if (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED) {
|
|
1371
|
+
} else if (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED && mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
|
|
1328
1372
|
if (mSnapInterval > 0) {
|
|
1329
1373
|
double ratio = (double) targetOffset / mSnapInterval;
|
|
1330
1374
|
smallerOffset =
|
|
@@ -157,6 +157,13 @@ constructor(private val fpsListener: FpsListener? = null) :
|
|
|
157
157
|
view.setSnapToEnd(snapToEnd)
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
@ReactProp(name = "snapToItemPadding")
|
|
161
|
+
public fun setSnapToItemPadding(view: ReactHorizontalScrollView, value: Float) {
|
|
162
|
+
val density = getDisplayMetricDensity()
|
|
163
|
+
val px = (value * density).toInt()
|
|
164
|
+
view.setSnapToItemPadding(px)
|
|
165
|
+
}
|
|
166
|
+
|
|
160
167
|
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
|
|
161
168
|
public fun setRemoveClippedSubviews(
|
|
162
169
|
view: ReactHorizontalScrollView,
|
|
@@ -10,6 +10,7 @@ package com.facebook.react.views.scroll;
|
|
|
10
10
|
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_CENTER;
|
|
11
11
|
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISABLED;
|
|
12
12
|
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END;
|
|
13
|
+
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_ITEM;
|
|
13
14
|
import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START;
|
|
14
15
|
import static com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView;
|
|
15
16
|
|
|
@@ -132,6 +133,7 @@ public class ReactScrollView extends ScrollView
|
|
|
132
133
|
private @Nullable MaintainVisibleScrollPositionHelper mMaintainVisibleContentPositionHelper;
|
|
133
134
|
private int mFadingEdgeLengthStart;
|
|
134
135
|
private int mFadingEdgeLengthEnd;
|
|
136
|
+
private int mSnapToItemPadding;
|
|
135
137
|
|
|
136
138
|
public ReactScrollView(Context context) {
|
|
137
139
|
this(context, null);
|
|
@@ -352,6 +354,10 @@ public class ReactScrollView extends ScrollView
|
|
|
352
354
|
invalidate();
|
|
353
355
|
}
|
|
354
356
|
|
|
357
|
+
public void setSnapToItemPadding(int snapToItemPadding) {
|
|
358
|
+
mSnapToItemPadding = snapToItemPadding;
|
|
359
|
+
}
|
|
360
|
+
|
|
355
361
|
@Override
|
|
356
362
|
protected float getTopFadingEdgeStrength() {
|
|
357
363
|
float max = Math.max(mFadingEdgeLengthStart, mFadingEdgeLengthEnd);
|
|
@@ -499,6 +505,40 @@ public class ReactScrollView extends ScrollView
|
|
|
499
505
|
return nextFocus;
|
|
500
506
|
}
|
|
501
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign.
|
|
510
|
+
* Returns true if snap scrolling was performed, false otherwise.
|
|
511
|
+
*/
|
|
512
|
+
private boolean tryScrollSnapToChild(View focused) {
|
|
513
|
+
if (mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
kotlin.Pair<View, String> result = ReactScrollViewHelper.findScrollSnapAlign(focused, this);
|
|
518
|
+
if (result == null) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
View snapTarget = result.getFirst();
|
|
523
|
+
String alignment = result.getSecond();
|
|
524
|
+
|
|
525
|
+
Rect rect = new Rect();
|
|
526
|
+
snapTarget.getDrawingRect(rect);
|
|
527
|
+
offsetDescendantRectToMyCoords(snapTarget, rect);
|
|
528
|
+
|
|
529
|
+
int viewportHeight = getHeight() - getPaddingTop() - getPaddingBottom();
|
|
530
|
+
int maxScrollY = getMaxScrollY();
|
|
531
|
+
|
|
532
|
+
Integer targetOffset = ReactScrollViewHelper.computeScrollSnapOffset(
|
|
533
|
+
rect.top, rect.bottom, viewportHeight, alignment, mSnapInterval, mSnapToItemPadding, maxScrollY);
|
|
534
|
+
if (targetOffset == null) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
reactSmoothScrollTo(getScrollX(), targetOffset);
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
|
|
502
542
|
/**
|
|
503
543
|
* Since ReactScrollView handles layout changes on JS side, it does not call super.onlayout due to
|
|
504
544
|
* which mIsLayoutDirty flag in ScrollView remains true and prevents scrolling to child when
|
|
@@ -509,7 +549,9 @@ public class ReactScrollView extends ScrollView
|
|
|
509
549
|
@Override
|
|
510
550
|
public void requestChildFocus(View child, View focused) {
|
|
511
551
|
if (focused != null) {
|
|
512
|
-
|
|
552
|
+
if (!tryScrollSnapToChild(focused)) {
|
|
553
|
+
scrollToChild(focused);
|
|
554
|
+
}
|
|
513
555
|
}
|
|
514
556
|
requestChildFocusWithoutScroll(child, focused);
|
|
515
557
|
}
|
|
@@ -686,7 +728,7 @@ public class ReactScrollView extends ScrollView
|
|
|
686
728
|
&& (mPagingEnabled
|
|
687
729
|
|| mSnapInterval != 0
|
|
688
730
|
|| mSnapOffsets != null
|
|
689
|
-
|| mSnapToAlignment != SNAP_ALIGNMENT_DISABLED)) {
|
|
731
|
+
|| (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED && mSnapToAlignment != SNAP_ALIGNMENT_ITEM))) {
|
|
690
732
|
// Cancel any pending post-touch runnable and reschedule
|
|
691
733
|
if (mPostTouchRunnable != null) {
|
|
692
734
|
removeCallbacks(mPostTouchRunnable);
|
|
@@ -1043,7 +1085,7 @@ public class ReactScrollView extends ScrollView
|
|
|
1043
1085
|
}
|
|
1044
1086
|
|
|
1045
1087
|
// pagingEnabled only allows snapping one interval at a time
|
|
1046
|
-
if (mSnapInterval == 0 && mSnapOffsets == null && mSnapToAlignment == SNAP_ALIGNMENT_DISABLED) {
|
|
1088
|
+
if (mSnapInterval == 0 && mSnapOffsets == null && (mSnapToAlignment == SNAP_ALIGNMENT_DISABLED || mSnapToAlignment == SNAP_ALIGNMENT_ITEM)) {
|
|
1047
1089
|
smoothScrollAndSnap(velocityY);
|
|
1048
1090
|
return;
|
|
1049
1091
|
}
|
|
@@ -1082,7 +1124,7 @@ public class ReactScrollView extends ScrollView
|
|
|
1082
1124
|
}
|
|
1083
1125
|
}
|
|
1084
1126
|
|
|
1085
|
-
} else if (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED) {
|
|
1127
|
+
} else if (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED && mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
|
|
1086
1128
|
if (mSnapInterval > 0) {
|
|
1087
1129
|
double ratio = (double) targetOffset / mSnapInterval;
|
|
1088
1130
|
smallerOffset =
|
|
@@ -11,6 +11,7 @@ import android.animation.Animator
|
|
|
11
11
|
import android.animation.ValueAnimator
|
|
12
12
|
import android.content.Context
|
|
13
13
|
import android.graphics.Point
|
|
14
|
+
import android.graphics.Rect
|
|
14
15
|
import android.os.Build
|
|
15
16
|
import android.view.View
|
|
16
17
|
import android.view.ViewGroup
|
|
@@ -32,6 +33,7 @@ import com.facebook.react.uimanager.StateWrapper
|
|
|
32
33
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
33
34
|
import com.facebook.react.uimanager.common.UIManagerType
|
|
34
35
|
import com.facebook.react.uimanager.common.ViewUtil
|
|
36
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
35
37
|
import java.lang.ref.WeakReference
|
|
36
38
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
37
39
|
import kotlin.math.abs
|
|
@@ -54,6 +56,7 @@ public object ReactScrollViewHelper {
|
|
|
54
56
|
public const val SNAP_ALIGNMENT_START: Int = 1
|
|
55
57
|
public const val SNAP_ALIGNMENT_CENTER: Int = 2
|
|
56
58
|
public const val SNAP_ALIGNMENT_END: Int = 3
|
|
59
|
+
public const val SNAP_ALIGNMENT_ITEM: Int = 4
|
|
57
60
|
|
|
58
61
|
// Support global native listeners for scroll events
|
|
59
62
|
private val scrollListeners = CopyOnWriteArrayList<WeakReference<ScrollListener>>()
|
|
@@ -209,6 +212,8 @@ public object ReactScrollViewHelper {
|
|
|
209
212
|
SNAP_ALIGNMENT_CENTER
|
|
210
213
|
} else if ("end" == alignment) {
|
|
211
214
|
SNAP_ALIGNMENT_END
|
|
215
|
+
} else if ("item".equals(alignment, ignoreCase = true)) {
|
|
216
|
+
SNAP_ALIGNMENT_ITEM
|
|
212
217
|
} else {
|
|
213
218
|
FLog.w(ReactConstants.TAG, "wrong snap alignment value: $alignment")
|
|
214
219
|
SNAP_ALIGNMENT_DISABLED
|
|
@@ -569,6 +574,70 @@ public object ReactScrollViewHelper {
|
|
|
569
574
|
return host.findViewById(nextFocusableViewId)
|
|
570
575
|
}
|
|
571
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Walks up the view hierarchy from the focused view to find a ReactViewGroup with
|
|
579
|
+
* scrollSnapAlign set. Returns a Pair of (snapTarget, alignment) or null if not found.
|
|
580
|
+
*
|
|
581
|
+
* Shared by [ReactScrollView] and [ReactHorizontalScrollView].
|
|
582
|
+
*/
|
|
583
|
+
@JvmStatic
|
|
584
|
+
public fun findScrollSnapAlign(focused: View, scrollView: ViewGroup): Pair<View, String>? {
|
|
585
|
+
var view: View? = focused
|
|
586
|
+
var snapTarget: View? = null
|
|
587
|
+
var alignment: String? = null
|
|
588
|
+
while (view != null && view !== scrollView) {
|
|
589
|
+
if (view is ReactViewGroup) {
|
|
590
|
+
val snap = view.scrollSnapAlign
|
|
591
|
+
if (snap != null) {
|
|
592
|
+
alignment = snap
|
|
593
|
+
snapTarget = view
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
val parent = view.parent
|
|
597
|
+
view = if (parent is View) parent else null
|
|
598
|
+
}
|
|
599
|
+
return if (alignment != null && snapTarget != null) Pair(snapTarget, alignment) else null
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Computes the target scroll offset for scroll-snap based on the focused view's position,
|
|
604
|
+
* alignment, snap interval, scroll padding, and maximum scroll range.
|
|
605
|
+
*
|
|
606
|
+
* Returns the clamped target offset, or null if the alignment is unknown.
|
|
607
|
+
*
|
|
608
|
+
* Shared by [ReactScrollView] and [ReactHorizontalScrollView].
|
|
609
|
+
*
|
|
610
|
+
* @param focusedStart the start coordinate of the snap target in scroll view coordinates
|
|
611
|
+
* @param focusedEnd the end coordinate of the snap target in scroll view coordinates
|
|
612
|
+
* @param viewportSize the visible viewport size on the scroll axis
|
|
613
|
+
* @param alignment the scrollSnapAlign value ("start", "center", or "end")
|
|
614
|
+
* @param snapInterval the snap interval (0 if not set)
|
|
615
|
+
* @param snapToItemPadding the snap-to-item padding value
|
|
616
|
+
* @param maxScrollOffset the maximum scroll offset for clamping
|
|
617
|
+
*/
|
|
618
|
+
@JvmStatic
|
|
619
|
+
public fun computeScrollSnapOffset(
|
|
620
|
+
focusedStart: Int,
|
|
621
|
+
focusedEnd: Int,
|
|
622
|
+
viewportSize: Int,
|
|
623
|
+
alignment: String,
|
|
624
|
+
snapInterval: Int,
|
|
625
|
+
snapToItemPadding: Int,
|
|
626
|
+
maxScrollOffset: Int,
|
|
627
|
+
): Int? {
|
|
628
|
+
val focusedCenter = (focusedStart + focusedEnd) / 2
|
|
629
|
+
var targetOffset = when (alignment) {
|
|
630
|
+
"start" -> focusedStart - snapToItemPadding
|
|
631
|
+
"center" -> focusedCenter - (viewportSize / 2) + (snapToItemPadding / 2)
|
|
632
|
+
"end" -> focusedEnd - viewportSize + snapToItemPadding
|
|
633
|
+
else -> return null
|
|
634
|
+
}
|
|
635
|
+
if (snapInterval > 0) {
|
|
636
|
+
targetOffset = (Math.floor(targetOffset.toDouble() / snapInterval) * snapInterval).toInt()
|
|
637
|
+
}
|
|
638
|
+
return Math.max(0, Math.min(targetOffset, maxScrollOffset))
|
|
639
|
+
}
|
|
640
|
+
|
|
572
641
|
@JvmStatic
|
|
573
642
|
public fun resolveAbsoluteDirection(
|
|
574
643
|
@FocusRealDirection direction: Int,
|
package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt
CHANGED
|
@@ -141,6 +141,13 @@ constructor(private val fpsListener: FpsListener? = null) :
|
|
|
141
141
|
view.setSnapToEnd(snapToEnd)
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
@ReactProp(name = "snapToItemPadding")
|
|
145
|
+
public fun setSnapToItemPadding(view: ReactScrollView, value: Float) {
|
|
146
|
+
val density = getDisplayMetricDensity()
|
|
147
|
+
val px = (value * density).toInt()
|
|
148
|
+
view.setSnapToItemPadding(px)
|
|
149
|
+
}
|
|
150
|
+
|
|
144
151
|
@ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
|
|
145
152
|
public fun setRemoveClippedSubviews(view: ReactScrollView, removeClippedSubviews: Boolean) {
|
|
146
153
|
view.removeClippedSubviews = removeClippedSubviews
|
|
@@ -173,6 +173,7 @@ public open class ReactViewGroup public constructor(context: Context?) :
|
|
|
173
173
|
private var childrenLayoutChangeListener: ChildrenLayoutChangeListener? = null
|
|
174
174
|
private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null
|
|
175
175
|
private var needsOffscreenAlphaCompositing = false
|
|
176
|
+
public var scrollSnapAlign: String? = null
|
|
176
177
|
private var backfaceOpacity = 0f
|
|
177
178
|
private var backfaceVisible = false
|
|
178
179
|
private var childrenRemovedWhileTransitioning: MutableSet<Int>? = null
|
|
@@ -190,6 +190,10 @@ public open class ReactViewManager : ReactClippingViewManager<ReactViewGroup>()
|
|
|
190
190
|
view.setTrapFocusRight(enabled)
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
@ReactProp(name = "scrollSnapAlign")
|
|
194
|
+
public open fun setScrollSnapAlign(view: ReactViewGroup, value: String?) {
|
|
195
|
+
view.scrollSnapAlign = value
|
|
196
|
+
}
|
|
193
197
|
|
|
194
198
|
@ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage")
|
|
195
199
|
public open fun setBackgroundImage(view: ReactViewGroup, backgroundImage: ReadableArray?) {
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
#define REACT_NATIVE_VERSION_MAJOR 0
|
|
16
16
|
#define REACT_NATIVE_VERSION_MINOR 83
|
|
17
|
-
#define REACT_NATIVE_VERSION_PATCH
|
|
17
|
+
#define REACT_NATIVE_VERSION_PATCH 4
|
|
18
18
|
|
|
19
19
|
namespace facebook::react {
|
|
20
20
|
|
|
21
21
|
constexpr struct {
|
|
22
22
|
int32_t Major = 0;
|
|
23
23
|
int32_t Minor = 83;
|
|
24
|
-
int32_t Patch =
|
|
24
|
+
int32_t Patch = 4;
|
|
25
25
|
std::string_view Prerelease = "0";
|
|
26
26
|
} ReactNativeVersion;
|
|
27
27
|
|
|
@@ -32,6 +32,15 @@ BaseScrollViewProps::BaseScrollViewProps(
|
|
|
32
32
|
"showsScrollIndex",
|
|
33
33
|
sourceProps.showsScrollIndex,
|
|
34
34
|
{})),
|
|
35
|
+
snapToItemPadding(
|
|
36
|
+
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
|
|
37
|
+
? sourceProps.snapToItemPadding
|
|
38
|
+
: convertRawProp(
|
|
39
|
+
context,
|
|
40
|
+
rawProps,
|
|
41
|
+
"snapToItemPadding",
|
|
42
|
+
sourceProps.snapToItemPadding,
|
|
43
|
+
(Float)0)),
|
|
35
44
|
#endif
|
|
36
45
|
alwaysBounceHorizontal(
|
|
37
46
|
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
|
|
@@ -400,6 +409,7 @@ void BaseScrollViewProps::setProp(
|
|
|
400
409
|
switch (hash) {
|
|
401
410
|
#if TARGET_OS_TV
|
|
402
411
|
RAW_SET_PROP_SWITCH_CASE_BASIC(showsScrollIndex);
|
|
412
|
+
RAW_SET_PROP_SWITCH_CASE_BASIC(snapToItemPadding);
|
|
403
413
|
#endif
|
|
404
414
|
RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceHorizontal);
|
|
405
415
|
RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceVertical);
|
|
@@ -30,6 +30,10 @@ inline void fromRawValue(const PropsParserContext &context, const RawValue &valu
|
|
|
30
30
|
result = ScrollViewSnapToAlignment::End;
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
+
if (string == "item") {
|
|
34
|
+
result = ScrollViewSnapToAlignment::Item;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
33
37
|
abort();
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -117,6 +121,8 @@ inline std::string toString(const ScrollViewSnapToAlignment &value)
|
|
|
117
121
|
return "center";
|
|
118
122
|
case ScrollViewSnapToAlignment::End:
|
|
119
123
|
return "end";
|
|
124
|
+
case ScrollViewSnapToAlignment::Item:
|
|
125
|
+
return "item";
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
|
|
@@ -65,7 +65,16 @@ HostPlatformScrollViewProps::HostPlatformScrollViewProps(
|
|
|
65
65
|
rawProps,
|
|
66
66
|
"endFillColor",
|
|
67
67
|
sourceProps.endFillColor,
|
|
68
|
-
clearColor()))
|
|
68
|
+
clearColor())),
|
|
69
|
+
snapToItemPadding(
|
|
70
|
+
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
|
|
71
|
+
? sourceProps.snapToItemPadding
|
|
72
|
+
: convertRawProp(
|
|
73
|
+
context,
|
|
74
|
+
rawProps,
|
|
75
|
+
"snapToItemPadding",
|
|
76
|
+
sourceProps.snapToItemPadding,
|
|
77
|
+
{})) {}
|
|
69
78
|
|
|
70
79
|
void HostPlatformScrollViewProps::setProp(
|
|
71
80
|
const PropsParserContext& context,
|
|
@@ -85,6 +94,7 @@ void HostPlatformScrollViewProps::setProp(
|
|
|
85
94
|
RAW_SET_PROP_SWITCH_CASE_BASIC(fadingEdgeLength);
|
|
86
95
|
RAW_SET_PROP_SWITCH_CASE_BASIC(overScrollMode);
|
|
87
96
|
RAW_SET_PROP_SWITCH_CASE_BASIC(endFillColor);
|
|
97
|
+
RAW_SET_PROP_SWITCH_CASE_BASIC(snapToItemPadding);
|
|
88
98
|
}
|
|
89
99
|
}
|
|
90
100
|
|
|
@@ -327,6 +337,9 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps(
|
|
|
327
337
|
case ScrollViewSnapToAlignment::End:
|
|
328
338
|
result["snapToAlignment"] = "end";
|
|
329
339
|
break;
|
|
340
|
+
case ScrollViewSnapToAlignment::Item:
|
|
341
|
+
result["snapToAlignment"] = "item";
|
|
342
|
+
break;
|
|
330
343
|
}
|
|
331
344
|
}
|
|
332
345
|
|
|
@@ -396,6 +409,10 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps(
|
|
|
396
409
|
result["endFillColor"] = *endFillColor;
|
|
397
410
|
}
|
|
398
411
|
|
|
412
|
+
if (snapToItemPadding != oldProps->snapToItemPadding) {
|
|
413
|
+
result["snapToItemPadding"] = snapToItemPadding;
|
|
414
|
+
}
|
|
415
|
+
|
|
399
416
|
return result;
|
|
400
417
|
}
|
|
401
418
|
|
|
@@ -33,6 +33,8 @@ class HostPlatformScrollViewProps : public BaseScrollViewProps {
|
|
|
33
33
|
std::string overScrollMode{"auto"};
|
|
34
34
|
SharedColor endFillColor{clearColor()};
|
|
35
35
|
|
|
36
|
+
Float snapToItemPadding{0};
|
|
37
|
+
|
|
36
38
|
#pragma mark - DebugStringConvertible
|
|
37
39
|
|
|
38
40
|
#if RN_DEBUG_STRING_CONVERTIBLE
|
|
@@ -447,7 +447,16 @@ BaseViewProps::BaseViewProps(
|
|
|
447
447
|
rawProps,
|
|
448
448
|
"removeClippedSubviews",
|
|
449
449
|
sourceProps.removeClippedSubviews,
|
|
450
|
-
false))
|
|
450
|
+
false))
|
|
451
|
+
#if TARGET_OS_TV
|
|
452
|
+
,scrollSnapAlign(ReactNativeFeatureFlags::enableCppPropsIteratorSetter() ? sourceProps.scrollSnapAlign : convertRawProp(
|
|
453
|
+
context,
|
|
454
|
+
rawProps,
|
|
455
|
+
"scrollSnapAlign",
|
|
456
|
+
sourceProps.scrollSnapAlign,
|
|
457
|
+
{}))
|
|
458
|
+
#endif
|
|
459
|
+
{}
|
|
451
460
|
|
|
452
461
|
#define VIEW_EVENT_CASE(eventType) \
|
|
453
462
|
case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \
|
|
@@ -518,6 +527,7 @@ void BaseViewProps::setProp(
|
|
|
518
527
|
RAW_SET_PROP_SWITCH_CASE_BASIC(trapFocusDown);
|
|
519
528
|
RAW_SET_PROP_SWITCH_CASE_BASIC(trapFocusLeft);
|
|
520
529
|
RAW_SET_PROP_SWITCH_CASE_BASIC(trapFocusRight);
|
|
530
|
+
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollSnapAlign);
|
|
521
531
|
#endif
|
|
522
532
|
// events field
|
|
523
533
|
VIEW_EVENT_CASE(PointerEnter);
|
|
@@ -124,6 +124,10 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps {
|
|
|
124
124
|
|
|
125
125
|
bool removeClippedSubviews{false};
|
|
126
126
|
|
|
127
|
+
#if TARGET_OS_TV
|
|
128
|
+
std::optional<std::string> scrollSnapAlign;
|
|
129
|
+
#endif
|
|
130
|
+
|
|
127
131
|
#pragma mark - Convenience Methods
|
|
128
132
|
|
|
129
133
|
CascadedBorderWidths getBorderWidths() const;
|
|
@@ -99,6 +99,15 @@ HostPlatformViewProps::HostPlatformViewProps(
|
|
|
99
99
|
"trapFocusRight",
|
|
100
100
|
sourceProps.trapFocusRight,
|
|
101
101
|
false)),
|
|
102
|
+
scrollSnapAlign(
|
|
103
|
+
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
|
|
104
|
+
? sourceProps.scrollSnapAlign
|
|
105
|
+
: convertRawProp(
|
|
106
|
+
context,
|
|
107
|
+
rawProps,
|
|
108
|
+
"scrollSnapAlign",
|
|
109
|
+
sourceProps.scrollSnapAlign,
|
|
110
|
+
{})),
|
|
102
111
|
needsOffscreenAlphaCompositing(
|
|
103
112
|
ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
|
|
104
113
|
? sourceProps.needsOffscreenAlphaCompositing
|
|
@@ -201,6 +210,7 @@ void HostPlatformViewProps::setProp(
|
|
|
201
210
|
RAW_SET_PROP_SWITCH_CASE(nativeForeground, "nativeForegroundAndroid");
|
|
202
211
|
RAW_SET_PROP_SWITCH_CASE_BASIC(focusable);
|
|
203
212
|
RAW_SET_PROP_SWITCH_CASE_BASIC(hasTVPreferredFocus);
|
|
213
|
+
RAW_SET_PROP_SWITCH_CASE_BASIC(scrollSnapAlign);
|
|
204
214
|
RAW_SET_PROP_SWITCH_CASE_BASIC(needsOffscreenAlphaCompositing);
|
|
205
215
|
RAW_SET_PROP_SWITCH_CASE_BASIC(renderToHardwareTextureAndroid);
|
|
206
216
|
RAW_SET_PROP_SWITCH_CASE_BASIC(screenReaderFocusable);
|
|
@@ -1105,6 +1115,14 @@ folly::dynamic HostPlatformViewProps::getDiffProps(
|
|
|
1105
1115
|
}
|
|
1106
1116
|
}
|
|
1107
1117
|
|
|
1118
|
+
if (scrollSnapAlign != oldProps->scrollSnapAlign) {
|
|
1119
|
+
if (scrollSnapAlign.has_value()) {
|
|
1120
|
+
result["scrollSnapAlign"] = scrollSnapAlign.value();
|
|
1121
|
+
} else {
|
|
1122
|
+
result["scrollSnapAlign"] = folly::dynamic(nullptr);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1108
1126
|
return result;
|
|
1109
1127
|
}
|
|
1110
1128
|
|
|
@@ -47,6 +47,7 @@ class HostPlatformViewProps : public BaseViewProps {
|
|
|
47
47
|
bool trapFocusDown{false};
|
|
48
48
|
bool trapFocusLeft{false};
|
|
49
49
|
bool trapFocusRight{false};
|
|
50
|
+
std::optional<std::string> scrollSnapAlign{};
|
|
50
51
|
|
|
51
52
|
bool needsOffscreenAlphaCompositing{false};
|
|
52
53
|
bool renderToHardwareTextureAndroid{false};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-tvos",
|
|
3
|
-
"version": "0.83.
|
|
3
|
+
"version": "0.83.4-0",
|
|
4
4
|
"description": "A framework for building native apps using React",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -165,12 +165,12 @@
|
|
|
165
165
|
},
|
|
166
166
|
"dependencies": {
|
|
167
167
|
"@jest/create-cache-key-function": "^29.7.0",
|
|
168
|
-
"@react-native/assets-registry": "0.83.
|
|
169
|
-
"@react-native/codegen": "0.83.
|
|
170
|
-
"@react-native/community-cli-plugin": "0.83.
|
|
171
|
-
"@react-native/gradle-plugin": "0.83.
|
|
172
|
-
"@react-native/js-polyfills": "0.83.
|
|
173
|
-
"@react-native/normalize-colors": "0.83.
|
|
168
|
+
"@react-native/assets-registry": "0.83.4",
|
|
169
|
+
"@react-native/codegen": "0.83.4",
|
|
170
|
+
"@react-native/community-cli-plugin": "0.83.4",
|
|
171
|
+
"@react-native/gradle-plugin": "0.83.4",
|
|
172
|
+
"@react-native/js-polyfills": "0.83.4",
|
|
173
|
+
"@react-native/normalize-colors": "0.83.4",
|
|
174
174
|
"abort-controller": "^3.0.0",
|
|
175
175
|
"anser": "^1.4.9",
|
|
176
176
|
"ansi-regex": "^5.0.0",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"whatwg-fetch": "^3.0.0",
|
|
199
199
|
"ws": "^7.5.10",
|
|
200
200
|
"yargs": "^17.6.2",
|
|
201
|
-
"@react-native-tvos/virtualized-lists": "0.83.
|
|
201
|
+
"@react-native-tvos/virtualized-lists": "0.83.4-0"
|
|
202
202
|
},
|
|
203
203
|
"codegenConfig": {
|
|
204
204
|
"libraries": [
|
|
@@ -4,7 +4,24 @@ import type { View, ScrollViewProps, HostComponent, EventSubscription, TVParalla
|
|
|
4
4
|
declare module 'react-native' {
|
|
5
5
|
export type FocusDestination = null | number | React.Component<any, any> | React.ComponentClass<any>;
|
|
6
6
|
|
|
7
|
+
interface ScrollViewProps {
|
|
8
|
+
/**
|
|
9
|
+
* Padding applied when snapping to items using `snapToAlignment="item"`.
|
|
10
|
+
* Set on the parent scroll view, not directly on child items.
|
|
11
|
+
* Only used when `snapToAlignment` is set to `'item'`.
|
|
12
|
+
*/
|
|
13
|
+
snapToItemPadding?: number | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
interface ViewProps {
|
|
17
|
+
/**
|
|
18
|
+
* Controls the scroll snap alignment when this view receives focus inside a ScrollView.
|
|
19
|
+
* Used with snapToAlignment="item" on the parent ScrollView.
|
|
20
|
+
*
|
|
21
|
+
* @platform tv
|
|
22
|
+
*/
|
|
23
|
+
scrollSnapAlign?: 'start' | 'center' | 'end' | undefined;
|
|
24
|
+
|
|
8
25
|
/**
|
|
9
26
|
* Android TV only prop
|
|
10
27
|
*/
|
|
@@ -54,8 +71,8 @@ declare module 'react-native' {
|
|
|
54
71
|
|
|
55
72
|
/**
|
|
56
73
|
* Hardware event received from TVEventHandler
|
|
57
|
-
*
|
|
58
|
-
* Note: The 'blur' and 'focus' event types are deprecated and will no longer be
|
|
74
|
+
*
|
|
75
|
+
* Note: The 'blur' and 'focus' event types are deprecated and will no longer be
|
|
59
76
|
* emitted on new architecture (Fabric). Use onFocus/onBlur component props instead.
|
|
60
77
|
* See: https://github.com/react-native-tvos/react-native-tvos/issues/1037
|
|
61
78
|
*/
|