react-native-tvos 0.83.4-0 → 0.83.4-2

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.
Files changed (29) hide show
  1. package/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js +1 -0
  2. package/Libraries/Components/ScrollView/ScrollView.d.ts +7 -0
  3. package/Libraries/Components/ScrollView/ScrollView.js +6 -0
  4. package/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +2 -0
  5. package/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +1 -0
  6. package/Libraries/Core/ReactNativeVersion.js +1 -1
  7. package/README.md +52 -0
  8. package/React/Base/RCTVersion.m +1 -1
  9. package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h +1 -0
  10. package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm +78 -0
  11. package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +32 -6
  12. package/React/Views/ScrollView/RCTScrollViewManager.m +1 -0
  13. package/ReactAndroid/gradle.properties +1 -1
  14. package/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +2 -2
  15. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
  16. package/ReactAndroid/src/main/java/com/facebook/react/views/common/UiModeUtils.kt +20 -0
  17. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +69 -8
  18. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt +5 -0
  19. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +30 -2
  20. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt +5 -0
  21. package/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +28 -3
  22. package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +1 -1
  23. package/ReactCommon/cxxreact/ReactNativeVersion.h +1 -1
  24. package/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +2 -2
  25. package/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp +10 -0
  26. package/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h +1 -0
  27. package/package.json +2 -2
  28. package/src/private/featureflags/ReactNativeFeatureFlags.js +2 -2
  29. package/types/public/ReactNativeTVTypes.d.ts +8 -0
@@ -59,6 +59,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
59
59
  process: require('../../StyleSheet/processColor').default,
60
60
  },
61
61
  snapToItemPadding: true,
62
+ scrollAnimationEnabled: true,
62
63
  pointerEvents: true,
63
64
  },
64
65
  };
@@ -745,6 +745,13 @@ export interface ScrollViewProps
745
745
  */
746
746
  removeClippedSubviews?: boolean | undefined;
747
747
 
748
+ /**
749
+ * (TV only)
750
+ * When false, the scroll view will jump to the correct offset without animation
751
+ * when focus changes. Defaults to true.
752
+ */
753
+ scrollAnimationEnabled?: boolean | undefined;
754
+
748
755
  /**
749
756
  * When true, shows a horizontal scroll indicator.
750
757
  */
@@ -662,6 +662,12 @@ type ScrollViewBaseProps = $ReadOnly<{
662
662
  * true.
663
663
  */
664
664
  removeClippedSubviews?: ?boolean,
665
+ /**
666
+ * (TV only)
667
+ * When false, the scroll view will jump to the correct offset without animation
668
+ * when focus changes. Defaults to true.
669
+ */
670
+ scrollAnimationEnabled?: ?boolean,
665
671
  /**
666
672
  * A RefreshControl component, used to provide pull-to-refresh
667
673
  * functionality for the ScrollView. Only works for vertical ScrollViews
@@ -87,6 +87,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
87
87
  process: require('../../StyleSheet/processColor').default,
88
88
  },
89
89
  snapToItemPadding: true,
90
+ scrollAnimationEnabled: true,
90
91
  pointerEvents: true,
91
92
  isInvertedVirtualizedList: true,
92
93
  },
@@ -152,6 +153,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
152
153
  scrollsToTop: true,
153
154
  showsHorizontalScrollIndicator: true,
154
155
  showsVerticalScrollIndicator: true,
156
+ scrollAnimationEnabled: true,
155
157
  showsScrollIndex: true,
156
158
  snapToItemPadding: true,
157
159
  snapToAlignment: true,
@@ -72,6 +72,7 @@ export type ScrollViewNativeProps = $ReadOnly<{
72
72
  snapToItemPadding?: ?number,
73
73
  sendMomentumEvents?: ?boolean,
74
74
  showsHorizontalScrollIndicator?: ?boolean,
75
+ scrollAnimationEnabled?: ?boolean,
75
76
  showsScrollIndex?: ?boolean,
76
77
  showsVerticalScrollIndicator?: ?boolean,
77
78
  snapToAlignment?: ?('start' | 'center' | 'end' | 'item'),
@@ -29,7 +29,7 @@ export default class ReactNativeVersion {
29
29
  static major: number = 0;
30
30
  static minor: number = 83;
31
31
  static patch: number = 4;
32
- static prerelease: string | null = '0';
32
+ static prerelease: string | null = '2';
33
33
 
34
34
  static getVersionString(): string {
35
35
  return `${this.major}.${this.minor}.${this.patch}${this.prerelease != null ? `-${this.prerelease}` : ''}`;
package/README.md CHANGED
@@ -262,6 +262,58 @@ class Game2048 extends React.Component {
262
262
 
263
263
  - _TVTextScrollView_: On Apple TV, a ScrollView will not scroll unless there are focusable items inside it or above/below it. This component works on both Apple TV and Android TV, using native code to allow scrolling using swipe gestures from the remote control.
264
264
 
265
+ - _ScrollView snap alignment_: The existing `snapToAlignment` prop has been extended with a new `'item'` value that enables per-item snap alignment on TV. Instead of snapping all items uniformly to `'start'`, `'center'`, or `'end'`, each child can specify its own alignment via the `scrollSnapAlign` View prop. Optional padding can be applied with `snapToItemPadding` on the ScrollView.
266
+
267
+ | Prop (ScrollView) | Value | Description |
268
+ |---|---|---|
269
+ | snapToAlignment | `'item'` | Enables per-item snap alignment (TV only). Requires `snapToInterval` to be set. |
270
+ | snapToItemPadding | number? | Extra padding (in dp/pt) applied around items when snapping. Only used with `snapToAlignment="item"`. |
271
+
272
+ | Prop (View) | Value | Description |
273
+ |---|---|---|
274
+ | scrollSnapAlign | `'start'` \| `'center'` \| `'end'` | Controls where this item snaps inside its parent ScrollView. Only used when the parent has `snapToAlignment="item"`. |
275
+
276
+ ```jsx
277
+ <ScrollView
278
+ horizontal
279
+ snapToInterval={300}
280
+ snapToAlignment="item"
281
+ snapToItemPadding={20}>
282
+ <Pressable scrollSnapAlign="start" style={{width: 300}}>
283
+ <Text>Snaps to start</Text>
284
+ </Pressable>
285
+ <Pressable scrollSnapAlign="center" style={{width: 200}}>
286
+ <Text>Snaps to center</Text>
287
+ </Pressable>
288
+ <Pressable scrollSnapAlign="end" style={{width: 300}}>
289
+ <Text>Snaps to end</Text>
290
+ </Pressable>
291
+ </ScrollView>
292
+ ```
293
+
294
+ - _ScrollView animation control_: The `scrollAnimationEnabled` prop allows disabling scroll animations when focus changes on TV. When set to `false`, the scroll view jumps instantly to the focused item instead of animating. This only affects TV platforms and has no effect on mobile.
295
+
296
+ | Prop (ScrollView) | Value | Description |
297
+ |---|---|---|
298
+ | scrollAnimationEnabled | boolean? | When `false`, disables animated scrolling on focus change. Defaults to `true`. TV only. |
299
+
300
+ ```jsx
301
+ <ScrollView horizontal scrollAnimationEnabled={false}>
302
+ <Pressable style={{width: 300}}><Text>Item 1</Text></Pressable>
303
+ <Pressable style={{width: 300}}><Text>Item 2</Text></Pressable>
304
+ <Pressable style={{width: 300}}><Text>Item 3</Text></Pressable>
305
+ </ScrollView>
306
+ ```
307
+
308
+ - _Interaction with existing ScrollView props_: The TV scroll props build on top of existing React Native ScrollView behavior. Here is how they interact:
309
+
310
+ - `snapToAlignment="item"` does not require `snapToInterval` to be set. It works as a standalone snapping mode where each child's `scrollSnapAlign` determines the snap position. If `snapToInterval` is also set, the interval is applied as an additional constraint after the item offset is computed.
311
+ - `snapToAlignment="item"` should not be combined with `pagingEnabled`. Both attempt to control scroll positioning independently, which leads to unpredictable behavior. Use one or the other.
312
+ - `snapToStart` and `snapToEnd` work independently of `snapToAlignment="item"`. They control edge behavior during swipe/drag momentum (whether the scroll view snaps to the first or last position), but do not affect focus driven item snapping.
313
+ - `scrollAnimationEnabled={false}` disables all scroll animations, including programmatic `scrollTo({animated: true})` calls. When disabled, all scrolling is instant.
314
+ - `decelerationRate` has no effect when `scrollAnimationEnabled={false}`, since there is no animation to decelerate.
315
+ - `scrollAnimationEnabled` and `snapToAlignment="item"` work well together. Snapping still occurs, but instantly instead of animated.
316
+
265
317
  - _VirtualizedList_: We extend `VirtualizedList` to make virtualization work well with focus management in mind. All of the improvements that we made are automatically available to all the VirtualizedList based components such as `FlatList`.
266
318
  - Defaults: VirtualizeList contents are automatically wrapped with a `TVFocusGuideView` with `trapFocus*` properties enabled depending on the orientation of the list. This default makes sure that focus doesn't leave the list accidentally due to a virtualization issue etc. until reaching the beginning or the end of the list.
267
319
  - New Props:
@@ -24,7 +24,7 @@ NSDictionary* RCTGetReactNativeVersion(void)
24
24
  RCTVersionMajor: @(0),
25
25
  RCTVersionMinor: @(83),
26
26
  RCTVersionPatch: @(4),
27
- RCTVersionPrerelease: @"0",
27
+ RCTVersionPrerelease: @"2",
28
28
  };
29
29
  });
30
30
  return __rnVersion;
@@ -52,6 +52,7 @@ NS_ASSUME_NONNULL_BEGIN
52
52
  @property (nonatomic, assign) BOOL snapToEnd;
53
53
  @property (nonatomic, copy) NSArray<NSNumber *> *snapToOffsets;
54
54
  @property (nonatomic, assign) BOOL scrollSnapEnabled;
55
+ @property (nonatomic, assign) BOOL scrollAnimationEnabled;
55
56
 
56
57
  /*
57
58
  * Makes `setContentOffset:` method no-op when given `block` is executed.
@@ -9,6 +9,28 @@
9
9
  #import <React/RCTUtils.h>
10
10
  #import <react/utils/FloatComparison.h>
11
11
 
12
+ #if TARGET_OS_TV
13
+ // Layer subclass that can block all animations when scrollAnimationEnabled is NO.
14
+ // This intercepts animations at the point they're added to the layer, which catches
15
+ // all animation sources: UIView animations, focus coordinator animations, and
16
+ // Core Animation implicit/explicit animations.
17
+ @interface RCTScrollViewLayer : CALayer
18
+ @property (nonatomic, assign) BOOL blockAnimations;
19
+ @end
20
+
21
+ @implementation RCTScrollViewLayer
22
+
23
+ - (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key
24
+ {
25
+ if (_blockAnimations) {
26
+ return;
27
+ }
28
+ [super addAnimation:anim forKey:key];
29
+ }
30
+
31
+ @end
32
+ #endif
33
+
12
34
  @interface RCTEnhancedScrollView () <UIScrollViewDelegate>
13
35
  @end
14
36
 
@@ -17,6 +39,13 @@
17
39
  BOOL _isSetContentOffsetDisabled;
18
40
  }
19
41
 
42
+ #if TARGET_OS_TV
43
+ + (Class)layerClass
44
+ {
45
+ return [RCTScrollViewLayer class];
46
+ }
47
+ #endif
48
+
20
49
  + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
21
50
  {
22
51
  if ([key isEqualToString:@"delegate"]) {
@@ -28,9 +57,18 @@
28
57
  return [super automaticallyNotifiesObserversForKey:key];
29
58
  }
30
59
 
60
+ - (void)setScrollAnimationEnabled:(BOOL)scrollAnimationEnabled
61
+ {
62
+ _scrollAnimationEnabled = scrollAnimationEnabled;
63
+ #if TARGET_OS_TV
64
+ ((RCTScrollViewLayer *)self.layer).blockAnimations = !scrollAnimationEnabled;
65
+ #endif
66
+ }
67
+
31
68
  - (instancetype)initWithFrame:(CGRect)frame
32
69
  {
33
70
  if (self = [super initWithFrame:frame]) {
71
+ _scrollAnimationEnabled = YES;
34
72
  // We set the default behavior to "never" so that iOS
35
73
  // doesn't do weird things to UIScrollView insets automatically
36
74
  // and keeps it as an opt-in behavior.
@@ -102,6 +140,46 @@
102
140
  RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
103
141
  }
104
142
 
143
+ - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
144
+ {
145
+ if (_isSetContentOffsetDisabled) {
146
+ return;
147
+ }
148
+ #if TARGET_OS_TV
149
+ animated = animated && _scrollAnimationEnabled;
150
+ #endif
151
+ [super setContentOffset:CGPointMake(
152
+ RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
153
+ RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"))
154
+ animated:animated];
155
+ }
156
+
157
+ - (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated
158
+ {
159
+ #if TARGET_OS_TV
160
+ animated = animated && _scrollAnimationEnabled;
161
+ #endif
162
+ [super scrollRectToVisible:rect animated:animated];
163
+ }
164
+
165
+ #if TARGET_OS_TV
166
+ - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context
167
+ withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
168
+ {
169
+ if (_scrollAnimationEnabled) {
170
+ [super didUpdateFocusInContext:context withAnimationCoordinator:coordinator];
171
+ return;
172
+ }
173
+
174
+ // When scrollAnimationEnabled is NO, don't call super and don't scroll here.
175
+ // RCTScrollViewComponentView.didUpdateFocusInContext handles all scrolling
176
+ // (both snap and non-snap cases) when animation is disabled. Scrolling here
177
+ // as well would cause conflicts — e.g. at the end of a snapToInterval list,
178
+ // this method computes a "just make visible" position while the component view
179
+ // computes the correct snapped position, causing a bounce effect.
180
+ }
181
+ #endif
182
+
105
183
  - (void)setFrame:(CGRect)frame
106
184
  {
107
185
  [super setFrame:frame];
@@ -434,6 +434,9 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
434
434
 
435
435
  scrollView.snapToStart = newScrollViewProps.snapToStart;
436
436
  scrollView.snapToEnd = newScrollViewProps.snapToEnd;
437
+ #if TARGET_OS_TV
438
+ scrollView.scrollAnimationEnabled = newScrollViewProps.scrollAnimationEnabled;
439
+ #endif
437
440
 
438
441
  if (oldScrollViewProps.snapToOffsets != newScrollViewProps.snapToOffsets) {
439
442
  NSMutableArray<NSNumber *> *snapToOffsets = [NSMutableArray array];
@@ -1235,7 +1238,7 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1235
1238
  ? CGPointMake(targetOffset, scrollView.contentOffset.y)
1236
1239
  : CGPointMake(scrollView.contentOffset.x, targetOffset);
1237
1240
  self.preferredContentOffset = targetContentOffset;
1238
- [_scrollView setContentOffset:targetContentOffset animated:YES];
1241
+ [_scrollView setContentOffset:targetContentOffset animated:scrollProps.scrollAnimationEnabled];
1239
1242
  }
1240
1243
 
1241
1244
  - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context
@@ -1244,7 +1247,18 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1244
1247
  self.preferredContentOffset = NO_PREFERRED_CONTENT_OFFSET;
1245
1248
  const auto &scrollProps = static_cast<const ScrollViewProps &>(*_props);
1246
1249
  BOOL hasItemSnapAlignment = scrollProps.snapToAlignment == ScrollViewSnapToAlignment::Item;
1247
- if (context.previouslyFocusedView == context.nextFocusedView || (!_props->isTVSelectable && !hasItemSnapAlignment)) {
1250
+ if (context.previouslyFocusedView == context.nextFocusedView) {
1251
+ return;
1252
+ }
1253
+ if (!_props->isTVSelectable && !hasItemSnapAlignment) {
1254
+ if (!scrollProps.scrollAnimationEnabled && [context.nextFocusedView isDescendantOfView:_scrollView]) {
1255
+ // When animations are disabled and there's no snap alignment, manually scroll
1256
+ // the focused view into view instantly. We can't let UIScrollView's default
1257
+ // focus handling do this because it uses deceleration (timer-based) that can't
1258
+ // be blocked by the animation-blocking layer.
1259
+ CGRect targetRect = [context.nextFocusedView convertRect:context.nextFocusedView.bounds toView:_scrollView];
1260
+ [_scrollView scrollRectToVisible:targetRect animated:NO];
1261
+ }
1248
1262
  return;
1249
1263
  }
1250
1264
  if (_props->isTVSelectable && context.nextFocusedView == self) {
@@ -1410,10 +1424,16 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1410
1424
  limitedOffset = MAX(limitedOffset, 0.0);
1411
1425
  limitedOffset = MIN(limitedOffset, maxOffset);
1412
1426
 
1413
- [UIView animateWithDuration:[self swipeDuration] animations:^{
1427
+ const auto &scrollProps = static_cast<const ScrollViewProps &>(*self->_props);
1428
+ if (scrollProps.scrollAnimationEnabled) {
1429
+ [UIView animateWithDuration:[self swipeDuration] animations:^{
1430
+ self.scrollView.contentOffset =
1431
+ CGPointMake(self.scrollView.contentOffset.x, limitedOffset);
1432
+ }];
1433
+ } else {
1414
1434
  self.scrollView.contentOffset =
1415
1435
  CGPointMake(self.scrollView.contentOffset.x, limitedOffset);
1416
- }];
1436
+ }
1417
1437
  });
1418
1438
  }
1419
1439
 
@@ -1434,10 +1454,16 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1434
1454
  limitedOffset = MAX(limitedOffset, 0.0);
1435
1455
  limitedOffset = MIN(limitedOffset, maxOffset);
1436
1456
 
1437
- [UIView animateWithDuration:[self swipeDuration] animations:^{
1457
+ const auto &scrollProps = static_cast<const ScrollViewProps &>(*self->_props);
1458
+ if (scrollProps.scrollAnimationEnabled) {
1459
+ [UIView animateWithDuration:[self swipeDuration] animations:^{
1460
+ self.scrollView.contentOffset =
1461
+ CGPointMake(limitedOffset, self.scrollView.contentOffset.y);
1462
+ }];
1463
+ } else {
1438
1464
  self.scrollView.contentOffset =
1439
1465
  CGPointMake(limitedOffset, self.scrollView.contentOffset.y);
1440
- }];
1466
+ }
1441
1467
  });
1442
1468
  }
1443
1469
 
@@ -106,6 +106,7 @@ RCT_EXPORT_VIEW_PROPERTY(onMomentumScrollEnd, RCTDirectEventBlock)
106
106
  RCT_EXPORT_VIEW_PROPERTY(inverted, BOOL)
107
107
  #if TARGET_OS_TV
108
108
  RCT_EXPORT_VIEW_PROPERTY(showsScrollIndex, BOOL)
109
+ RCT_EXPORT_VIEW_PROPERTY(scrollAnimationEnabled, BOOL)
109
110
  #endif
110
111
  RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustsScrollIndicatorInsets, BOOL)
111
112
  RCT_EXPORT_VIEW_PROPERTY(contentInsetAdjustmentBehavior, UIScrollViewContentInsetAdjustmentBehavior)
@@ -1,4 +1,4 @@
1
- VERSION_NAME=0.83.4-0
1
+ VERSION_NAME=0.83.4-2
2
2
  react.internal.publishingGroup=io.github.react-native-tvos
3
3
  react.internal.hermesPublishingGroup=com.facebook.hermes
4
4
 
@@ -4,7 +4,7 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
- * @generated SignedSource<<303c55a883b4798288716d168ce82d06>>
7
+ * @generated SignedSource<<5a6e678b586f0ad291c6a4d090dce5b5>>
8
8
  */
9
9
 
10
10
  /**
@@ -53,7 +53,7 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
53
53
 
54
54
  override fun enableCppPropsIteratorSetter(): Boolean = false
55
55
 
56
- override fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean = true
56
+ override fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean = false
57
57
 
58
58
  override fun enableDestroyShadowTreeRevisionAsync(): Boolean = false
59
59
 
@@ -15,6 +15,6 @@ public object ReactNativeVersion {
15
15
  "major" to 0,
16
16
  "minor" to 83,
17
17
  "patch" to 4,
18
- "prerelease" to "0"
18
+ "prerelease" to "2"
19
19
  )
20
20
  }
@@ -7,12 +7,32 @@
7
7
 
8
8
  package com.facebook.react.views.common
9
9
 
10
+ import android.app.UiModeManager
10
11
  import android.content.Context
11
12
  import android.content.res.Configuration
12
13
 
13
14
  /** Utility object providing static methods for working with UI mode properties from Context. */
14
15
  internal object UiModeUtils {
15
16
 
17
+ private var isTVDeviceCached: Boolean? = null
18
+
19
+ /**
20
+ * Determines whether the device is a TV (Android TV / Fire TV).
21
+ * The result is cached after the first call.
22
+ *
23
+ * @param context The context to check the UI mode from
24
+ * @return true if the device is running in television mode, false otherwise
25
+ */
26
+ @JvmStatic
27
+ fun isTVDevice(context: Context): Boolean {
28
+ return isTVDeviceCached ?: run {
29
+ val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as? UiModeManager
30
+ val result = uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
31
+ isTVDeviceCached = result
32
+ result
33
+ }
34
+ }
35
+
16
36
  /**
17
37
  * Determines whether the current UI mode is dark mode
18
38
  *
@@ -29,6 +29,7 @@ import android.view.KeyEvent;
29
29
  import android.view.MotionEvent;
30
30
  import android.view.View;
31
31
  import android.view.ViewGroup;
32
+ import android.view.ViewParent;
32
33
  import android.view.accessibility.AccessibilityNodeInfo;
33
34
  import android.widget.HorizontalScrollView;
34
35
  import android.widget.OverScroller;
@@ -38,6 +39,7 @@ import androidx.core.view.ViewCompat.FocusDirection;
38
39
  import com.facebook.common.logging.FLog;
39
40
  import com.facebook.infer.annotation.Assertions;
40
41
  import com.facebook.infer.annotation.Nullsafe;
42
+ import com.facebook.react.views.common.UiModeUtils;
41
43
  import com.facebook.react.R;
42
44
  import com.facebook.react.common.ReactConstants;
43
45
  import com.facebook.react.common.build.ReactBuildConfig;
@@ -136,6 +138,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
136
138
  private int mFadingEdgeLengthStart = 0;
137
139
  private int mFadingEdgeLengthEnd = 0;
138
140
  private int mSnapToItemPadding;
141
+ private boolean mScrollAnimationEnabled = true;
142
+ private boolean mBlockScrollDelta = false;
139
143
 
140
144
  public ReactHorizontalScrollView(Context context) {
141
145
  this(context, null);
@@ -247,9 +251,21 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
247
251
  return mScrollEnabled && super.canScrollHorizontally(direction);
248
252
  }
249
253
 
254
+ @Override
255
+ public void computeScroll() {
256
+ if (UiModeUtils.isTVDevice(getContext())
257
+ && !mScrollAnimationEnabled
258
+ && mScroller != null
259
+ && !mScroller.isFinished()) {
260
+ mScroller.forceFinished(true);
261
+ return;
262
+ }
263
+ super.computeScroll();
264
+ }
265
+
250
266
  @Override
251
267
  protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
252
- if (!mScrollEnabled) {
268
+ if (!mScrollEnabled || mBlockScrollDelta) {
253
269
  return 0;
254
270
  }
255
271
  return super.computeScrollDeltaToGetChildRectOnScreen(rect);
@@ -388,6 +404,10 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
388
404
  mSnapToItemPadding = snapToItemPadding;
389
405
  }
390
406
 
407
+ public void setScrollAnimationEnabled(boolean scrollAnimationEnabled) {
408
+ mScrollAnimationEnabled = scrollAnimationEnabled;
409
+ }
410
+
391
411
  @Override
392
412
  protected float getLeftFadingEdgeStrength() {
393
413
  float max = Math.max(mFadingEdgeLengthStart, mFadingEdgeLengthEnd);
@@ -603,7 +623,11 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
603
623
  * unblocks such customization.
604
624
  */
605
625
  protected void requestChildFocusWithoutScroll(View child, View focused) {
626
+ // Temporarily block HorizontalScrollView's internal scrollToChild from running
627
+ // during super.requestChildFocus — we've already handled scrolling in requestChildFocus.
628
+ mBlockScrollDelta = true;
606
629
  super.requestChildFocus(child, focused);
630
+ mBlockScrollDelta = false;
607
631
  }
608
632
 
609
633
  @Override
@@ -654,6 +678,18 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
654
678
  return scrollDelta != 0 && Math.abs(scrollDelta) < (mTempRect.width() / 2);
655
679
  }
656
680
 
681
+ /** Returns whether the given view is a descendant of the given ancestor */
682
+ private static boolean isDescendantOf(View view, View ancestor) {
683
+ ViewParent parent = view.getParent();
684
+ while (parent != null) {
685
+ if (parent == ancestor) {
686
+ return true;
687
+ }
688
+ parent = parent.getParent();
689
+ }
690
+ return false;
691
+ }
692
+
657
693
  private void scrollToChild(View child) {
658
694
  int scrollDelta = getScrollDelta(child);
659
695
 
@@ -796,21 +832,42 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
796
832
  public boolean arrowScroll(int direction) {
797
833
  boolean handled = false;
798
834
 
799
- if (mPagingEnabled) {
835
+ if (!mScrollAnimationEnabled) {
836
+ // When scroll animation is disabled, find the next focusable and request focus directly.
837
+ // This must come before the mPagingEnabled check because snapToInterval on Android
838
+ // auto-enables paging, and the paging branch uses smoothScrollToNextPage which
839
+ // scrolls by full page width instead of by snap interval.
840
+ // requestChildFocus → tryScrollSnapToChild/scrollToChild handles instant scrolling.
841
+ View currentFocused = findFocus();
842
+ View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
843
+ if (nextFocused != null && nextFocused != currentFocused && nextFocused != this) {
844
+ nextFocused.requestFocus(direction);
845
+ handled = true;
846
+ }
847
+ } else if (mPagingEnabled) {
800
848
  mPagedArrowScrolling = true;
801
849
 
802
850
  if (getChildCount() > 0) {
803
851
  View currentFocused = findFocus();
804
852
  View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
805
853
  View rootChild = getContentView();
806
- if (rootChild != null && nextFocused != null && nextFocused.getParent() == rootChild) {
807
- if (!isScrolledInView(nextFocused) && !isMostlyScrolledInView(nextFocused)) {
808
- smoothScrollToNextPage(direction);
854
+ if (rootChild != null && nextFocused != null && isDescendantOf(nextFocused, rootChild)) {
855
+ if (mSnapToAlignment == SNAP_ALIGNMENT_ITEM) {
856
+ // When snapToAlignment is "item", don't use smoothScrollToNextPage (which scrolls
857
+ // by full page width and ignores snapToItemPadding). Instead just request focus —
858
+ // requestChildFocus → tryScrollSnapToChild handles scrolling with correct padding.
859
+ nextFocused.requestFocus();
860
+ } else {
861
+ if (!isScrolledInView(nextFocused) && !isMostlyScrolledInView(nextFocused)) {
862
+ smoothScrollToNextPage(direction);
863
+ }
864
+ nextFocused.requestFocus();
809
865
  }
810
- nextFocused.requestFocus();
811
866
  handled = true;
812
867
  } else {
813
- smoothScrollToNextPage(direction);
868
+ if (mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
869
+ smoothScrollToNextPage(direction);
870
+ }
814
871
  handled = true;
815
872
  }
816
873
  }
@@ -1595,7 +1652,11 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
1595
1652
  * scroll view and state. Calling raw `smoothScrollTo` doesn't update state.
1596
1653
  */
1597
1654
  public void reactSmoothScrollTo(int x, int y) {
1598
- ReactScrollViewHelper.smoothScrollTo(this, x, y);
1655
+ if (mScrollAnimationEnabled || !UiModeUtils.isTVDevice(getContext())) {
1656
+ ReactScrollViewHelper.smoothScrollTo(this, x, y);
1657
+ } else {
1658
+ scrollTo(x, y);
1659
+ }
1599
1660
  setPendingContentOffsets(x, y);
1600
1661
  }
1601
1662
 
@@ -164,6 +164,11 @@ constructor(private val fpsListener: FpsListener? = null) :
164
164
  view.setSnapToItemPadding(px)
165
165
  }
166
166
 
167
+ @ReactProp(name = "scrollAnimationEnabled", defaultBoolean = true)
168
+ public fun setScrollAnimationEnabled(view: ReactHorizontalScrollView, value: Boolean) {
169
+ view.setScrollAnimationEnabled(value)
170
+ }
171
+
167
172
  @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
168
173
  public fun setRemoveClippedSubviews(
169
174
  view: ReactHorizontalScrollView,
@@ -38,6 +38,7 @@ import androidx.core.view.ViewCompat.FocusDirection;
38
38
  import com.facebook.common.logging.FLog;
39
39
  import com.facebook.infer.annotation.Assertions;
40
40
  import com.facebook.infer.annotation.Nullsafe;
41
+ import com.facebook.react.views.common.UiModeUtils;
41
42
  import com.facebook.react.R;
42
43
  import com.facebook.react.bridge.ReadableMap;
43
44
  import com.facebook.react.common.ReactConstants;
@@ -134,6 +135,8 @@ public class ReactScrollView extends ScrollView
134
135
  private int mFadingEdgeLengthStart;
135
136
  private int mFadingEdgeLengthEnd;
136
137
  private int mSnapToItemPadding;
138
+ private boolean mScrollAnimationEnabled = true;
139
+ private boolean mBlockScrollDelta = false;
137
140
 
138
141
  public ReactScrollView(Context context) {
139
142
  this(context, null);
@@ -358,6 +361,10 @@ public class ReactScrollView extends ScrollView
358
361
  mSnapToItemPadding = snapToItemPadding;
359
362
  }
360
363
 
364
+ public void setScrollAnimationEnabled(boolean scrollAnimationEnabled) {
365
+ mScrollAnimationEnabled = scrollAnimationEnabled;
366
+ }
367
+
361
368
  @Override
362
369
  protected float getTopFadingEdgeStrength() {
363
370
  float max = Math.max(mFadingEdgeLengthStart, mFadingEdgeLengthEnd);
@@ -562,7 +569,9 @@ public class ReactScrollView extends ScrollView
562
569
  * unblocks such customization.
563
570
  */
564
571
  protected void requestChildFocusWithoutScroll(View child, View focused) {
572
+ mBlockScrollDelta = true;
565
573
  super.requestChildFocus(child, focused);
574
+ mBlockScrollDelta = false;
566
575
  }
567
576
 
568
577
  private int getScrollDelta(View descendent) {
@@ -606,9 +615,24 @@ public class ReactScrollView extends ScrollView
606
615
  }
607
616
  }
608
617
 
618
+ @Override
619
+ public void computeScroll() {
620
+ if (UiModeUtils.isTVDevice(getContext())
621
+ && !mScrollAnimationEnabled
622
+ && mScroller != null
623
+ && !mScroller.isFinished()) {
624
+ // When scroll animation is disabled, just abort any in-flight smooth scroll.
625
+ // The correct position has already been set synchronously by
626
+ // requestChildFocus → tryScrollSnapToChild/scrollToChild.
627
+ mScroller.forceFinished(true);
628
+ return;
629
+ }
630
+ super.computeScroll();
631
+ }
632
+
609
633
  @Override
610
634
  protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
611
- if (!mScrollEnabled) {
635
+ if (!mScrollEnabled || mBlockScrollDelta) {
612
636
  return 0;
613
637
  }
614
638
  return super.computeScrollDeltaToGetChildRectOnScreen(rect);
@@ -1362,7 +1386,11 @@ public class ReactScrollView extends ScrollView
1362
1386
  * scroll view and state. Calling raw `smoothScrollTo` doesn't update state.
1363
1387
  */
1364
1388
  public void reactSmoothScrollTo(int x, int y) {
1365
- ReactScrollViewHelper.smoothScrollTo(this, x, y);
1389
+ if (mScrollAnimationEnabled || !UiModeUtils.isTVDevice(getContext())) {
1390
+ ReactScrollViewHelper.smoothScrollTo(this, x, y);
1391
+ } else {
1392
+ scrollTo(x, y);
1393
+ }
1366
1394
  setPendingContentOffsets(x, y);
1367
1395
  }
1368
1396
 
@@ -148,6 +148,11 @@ constructor(private val fpsListener: FpsListener? = null) :
148
148
  view.setSnapToItemPadding(px)
149
149
  }
150
150
 
151
+ @ReactProp(name = "scrollAnimationEnabled", defaultBoolean = true)
152
+ public fun setScrollAnimationEnabled(view: ReactScrollView, value: Boolean) {
153
+ view.setScrollAnimationEnabled(value)
154
+ }
155
+
151
156
  @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
152
157
  public fun setRemoveClippedSubviews(view: ReactScrollView, removeClippedSubviews: Boolean) {
153
158
  view.removeClippedSubviews = removeClippedSubviews
@@ -30,6 +30,7 @@ import androidx.appcompat.widget.TintContextWrapper;
30
30
  import androidx.core.view.AccessibilityDelegateCompat;
31
31
  import androidx.core.view.ViewCompat;
32
32
  import androidx.customview.widget.ExploreByTouchHelper;
33
+ import com.facebook.react.views.common.UiModeUtils;
33
34
  import com.facebook.common.logging.FLog;
34
35
  import com.facebook.infer.annotation.Assertions;
35
36
  import com.facebook.infer.annotation.Nullsafe;
@@ -151,14 +152,38 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie
151
152
  // mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED |
152
153
  // LAYOUT_DIRECTION_INHERIT;
153
154
  setEnabled(true);
154
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
155
- setFocusable(View.FOCUSABLE_AUTO);
156
- }
155
+ // Changed from FOCUSABLE_AUTO to NOT_FOCUSABLE to prevent Android TV's
156
+ // focus engine from landing on text views when no other views are available
157
+ setFocusable(View.NOT_FOCUSABLE);
157
158
 
158
159
  setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE);
159
160
  updateView(); // call after changing ellipsizeLocation in particular
160
161
  }
161
162
 
163
+ // On Android TV, prevent text views from becoming focusable.
164
+ // ReactTextAnchorViewManager.setAccessible() ties isFocusable to the
165
+ // "accessible" prop, which causes Android TV's D-pad focus engine to land
166
+ // on text views inside recycled list items (FlashList), triggering
167
+ // requestChildFocus -> scrollToChild scroll jumps.
168
+ // On non-TV devices, focusability is left unchanged for accessibility.
169
+ @Override
170
+ public void setFocusable(boolean focusable) {
171
+ if (UiModeUtils.isTVDevice(getContext())) {
172
+ super.setFocusable(false);
173
+ } else {
174
+ super.setFocusable(focusable);
175
+ }
176
+ }
177
+
178
+ @Override
179
+ public void setFocusable(int focusable) {
180
+ if (UiModeUtils.isTVDevice(getContext())) {
181
+ super.setFocusable(View.NOT_FOCUSABLE);
182
+ } else {
183
+ super.setFocusable(focusable);
184
+ }
185
+ }
186
+
162
187
  private static WritableMap inlineViewJson(
163
188
  int visibility, int index, int left, int top, int right, int bottom) {
164
189
  WritableMap json = Arguments.createMap();
@@ -631,7 +631,7 @@ public open class ReactViewGroup public constructor(context: Context?) :
631
631
  index++
632
632
  }
633
633
 
634
- return firstFocusableElement!!
634
+ return firstFocusableElement
635
635
  }
636
636
 
637
637
  private fun moveFocusToFirstFocusable(viewGroup: ReactViewGroup): Boolean {
@@ -22,7 +22,7 @@ constexpr struct {
22
22
  int32_t Major = 0;
23
23
  int32_t Minor = 83;
24
24
  int32_t Patch = 4;
25
- std::string_view Prerelease = "0";
25
+ std::string_view Prerelease = "2";
26
26
  } ReactNativeVersion;
27
27
 
28
28
  } // namespace facebook::react
@@ -4,7 +4,7 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
- * @generated SignedSource<<f8c2279957d1c654502ea5aa0f66beba>>
7
+ * @generated SignedSource<<044be89d81fef6507ed9fe1638a49a78>>
8
8
  */
9
9
 
10
10
  /**
@@ -88,7 +88,7 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider {
88
88
  }
89
89
 
90
90
  bool enableCustomFocusSearchOnClippedElementsAndroid() override {
91
- return true;
91
+ return false;
92
92
  }
93
93
 
94
94
  bool enableDestroyShadowTreeRevisionAsync() override {
@@ -41,6 +41,15 @@ BaseScrollViewProps::BaseScrollViewProps(
41
41
  "snapToItemPadding",
42
42
  sourceProps.snapToItemPadding,
43
43
  (Float)0)),
44
+ scrollAnimationEnabled(
45
+ ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
46
+ ? sourceProps.scrollAnimationEnabled
47
+ : convertRawProp(
48
+ context,
49
+ rawProps,
50
+ "scrollAnimationEnabled",
51
+ sourceProps.scrollAnimationEnabled,
52
+ true)),
44
53
  #endif
45
54
  alwaysBounceHorizontal(
46
55
  ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
@@ -410,6 +419,7 @@ void BaseScrollViewProps::setProp(
410
419
  #if TARGET_OS_TV
411
420
  RAW_SET_PROP_SWITCH_CASE_BASIC(showsScrollIndex);
412
421
  RAW_SET_PROP_SWITCH_CASE_BASIC(snapToItemPadding);
422
+ RAW_SET_PROP_SWITCH_CASE_BASIC(scrollAnimationEnabled);
413
423
  #endif
414
424
  RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceHorizontal);
415
425
  RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceVertical);
@@ -32,6 +32,7 @@ class BaseScrollViewProps : public ViewProps {
32
32
  #if TARGET_OS_TV
33
33
  bool showsScrollIndex{true};
34
34
  Float snapToItemPadding{0};
35
+ bool scrollAnimationEnabled{true};
35
36
  #endif
36
37
  bool alwaysBounceHorizontal{};
37
38
  bool alwaysBounceVertical{};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-tvos",
3
- "version": "0.83.4-0",
3
+ "version": "0.83.4-2",
4
4
  "description": "A framework for building native apps using React",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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.4-0"
201
+ "@react-native-tvos/virtualized-lists": "0.83.4-2"
202
202
  },
203
203
  "codegenConfig": {
204
204
  "libraries": [
@@ -4,7 +4,7 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
- * @generated SignedSource<<474b28e49cdb8a46a361a32df3c92eed>>
7
+ * @generated SignedSource<<c7dd7afe6bc76c1abb2b0499d3740ec2>>
8
8
  * @flow strict
9
9
  * @noformat
10
10
  */
@@ -279,7 +279,7 @@ export const enableCppPropsIteratorSetter: Getter<boolean> = createNativeFlagGet
279
279
  /**
280
280
  * This enables the fabric implementation of focus search so that we can focus clipped elements
281
281
  */
282
- export const enableCustomFocusSearchOnClippedElementsAndroid: Getter<boolean> = createNativeFlagGetter('enableCustomFocusSearchOnClippedElementsAndroid', true);
282
+ export const enableCustomFocusSearchOnClippedElementsAndroid: Getter<boolean> = createNativeFlagGetter('enableCustomFocusSearchOnClippedElementsAndroid', false);
283
283
  /**
284
284
  * Enables destructor calls for ShadowTreeRevision in the background to reduce UI thread work.
285
285
  */
@@ -11,6 +11,14 @@ declare module 'react-native' {
11
11
  * Only used when `snapToAlignment` is set to `'item'`.
12
12
  */
13
13
  snapToItemPadding?: number | undefined;
14
+
15
+ /**
16
+ * When false, the scroll view will jump to the correct offset without animation
17
+ * when focus changes. Defaults to true.
18
+ *
19
+ * @platform tv
20
+ */
21
+ scrollAnimationEnabled?: boolean | undefined;
14
22
  }
15
23
 
16
24
  interface ViewProps {