Better RTL support especially for ScrollView's
Reviewed By: fkgozali Differential Revision: D4478913 fbshipit-source-id: 525c17fa109ad3c35161b10940776f1426ba2535
This commit is contained in:
parent
d82f2553fb
commit
91910d87de
|
@ -483,6 +483,30 @@ const ScrollView = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
let ScrollViewClass;
|
||||
let ScrollContentContainerViewClass;
|
||||
if (Platform.OS === 'ios') {
|
||||
ScrollViewClass = RCTScrollView;
|
||||
ScrollContentContainerViewClass = RCTScrollContentView;
|
||||
} else if (Platform.OS === 'android') {
|
||||
if (this.props.horizontal) {
|
||||
ScrollViewClass = AndroidHorizontalScrollView;
|
||||
} else {
|
||||
ScrollViewClass = AndroidScrollView;
|
||||
}
|
||||
ScrollContentContainerViewClass = View;
|
||||
}
|
||||
|
||||
invariant(
|
||||
ScrollViewClass !== undefined,
|
||||
'ScrollViewClass must not be undefined'
|
||||
);
|
||||
|
||||
invariant(
|
||||
ScrollContentContainerViewClass !== undefined,
|
||||
'ScrollContentContainerViewClass must not be undefined'
|
||||
);
|
||||
|
||||
const contentContainerStyle = [
|
||||
this.props.horizontal && styles.contentContainerHorizontal,
|
||||
this.props.contentContainerStyle,
|
||||
|
@ -507,14 +531,14 @@ const ScrollView = React.createClass({
|
|||
}
|
||||
|
||||
const contentContainer =
|
||||
<View
|
||||
<ScrollContentContainerViewClass
|
||||
{...contentSizeChangeProps}
|
||||
ref={this._setInnerViewRef}
|
||||
style={contentContainerStyle}
|
||||
removeClippedSubviews={this.props.removeClippedSubviews}
|
||||
collapsable={false}>
|
||||
{this.props.children}
|
||||
</View>;
|
||||
</ScrollContentContainerViewClass>;
|
||||
|
||||
const alwaysBounceHorizontal =
|
||||
this.props.alwaysBounceHorizontal !== undefined ?
|
||||
|
@ -559,21 +583,6 @@ const ScrollView = React.createClass({
|
|||
props.decelerationRate = processDecelerationRate(decelerationRate);
|
||||
}
|
||||
|
||||
let ScrollViewClass;
|
||||
if (Platform.OS === 'ios') {
|
||||
ScrollViewClass = RCTScrollView;
|
||||
} else if (Platform.OS === 'android') {
|
||||
if (this.props.horizontal) {
|
||||
ScrollViewClass = AndroidHorizontalScrollView;
|
||||
} else {
|
||||
ScrollViewClass = AndroidScrollView;
|
||||
}
|
||||
}
|
||||
invariant(
|
||||
ScrollViewClass !== undefined,
|
||||
'ScrollViewClass must not be undefined'
|
||||
);
|
||||
|
||||
const refreshControl = this.props.refreshControl;
|
||||
if (refreshControl) {
|
||||
if (Platform.OS === 'ios') {
|
||||
|
@ -626,7 +635,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
let nativeOnlyProps, AndroidScrollView, AndroidHorizontalScrollView, RCTScrollView;
|
||||
let nativeOnlyProps, AndroidScrollView, AndroidHorizontalScrollView, RCTScrollView, RCTScrollContentView;
|
||||
if (Platform.OS === 'android') {
|
||||
nativeOnlyProps = {
|
||||
nativeOnly: {
|
||||
|
@ -649,6 +658,7 @@ if (Platform.OS === 'android') {
|
|||
}
|
||||
};
|
||||
RCTScrollView = requireNativeComponent('RCTScrollView', ScrollView, nativeOnlyProps);
|
||||
RCTScrollContentView = requireNativeComponent('RCTScrollContentView', View);
|
||||
}
|
||||
|
||||
module.exports = ScrollView;
|
||||
|
|
|
@ -227,6 +227,7 @@ static void RCTUpdatePlaceholder(RCTTextField *self)
|
|||
key:nil
|
||||
eventCount:_nativeEventCount];
|
||||
}
|
||||
|
||||
- (void)textFieldSubmitEditing
|
||||
{
|
||||
_submitted = YES;
|
||||
|
|
|
@ -552,6 +552,7 @@ dispatch_queue_t RCTGetUIManagerQueue(void)
|
|||
|
||||
typedef struct {
|
||||
CGRect frame;
|
||||
UIUserInterfaceLayoutDirection layoutDirection;
|
||||
BOOL isNew;
|
||||
BOOL parentIsNew;
|
||||
BOOL isHidden;
|
||||
|
@ -568,6 +569,7 @@ dispatch_queue_t RCTGetUIManagerQueue(void)
|
|||
reactTags[index] = shadowView.reactTag;
|
||||
frameDataArray[index++] = (RCTFrameData){
|
||||
shadowView.frame,
|
||||
shadowView.effectiveLayoutDirection,
|
||||
shadowView.isNewView,
|
||||
shadowView.superview.isNewView,
|
||||
shadowView.isHidden,
|
||||
|
@ -634,6 +636,7 @@ dispatch_queue_t RCTGetUIManagerQueue(void)
|
|||
CGRect frame = frameData.frame;
|
||||
|
||||
BOOL isHidden = frameData.isHidden;
|
||||
UIUserInterfaceLayoutDirection layoutDirection = frameData.layoutDirection;
|
||||
BOOL isNew = frameData.isNew;
|
||||
RCTAnimation *updateAnimation = isNew ? nil : layoutAnimation.updateAnimation;
|
||||
BOOL shouldAnimateCreation = isNew && !frameData.parentIsNew;
|
||||
|
@ -654,6 +657,10 @@ dispatch_queue_t RCTGetUIManagerQueue(void)
|
|||
view.hidden = isHidden;
|
||||
}
|
||||
|
||||
if (view.reactLayoutDirection != layoutDirection) {
|
||||
view.reactLayoutDirection = layoutDirection;
|
||||
}
|
||||
|
||||
RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag];
|
||||
if (createAnimation) {
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import <React/RCTShadowView.h>
|
||||
|
||||
@interface RCTScrollContentShadowView : RCTShadowView
|
||||
|
||||
@end
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import "RCTScrollContentShadowView.h"
|
||||
|
||||
#import <yoga/Yoga.h>
|
||||
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@interface RCTShadowView () {
|
||||
// This will be removed after t15757916, which will remove
|
||||
// side-effects from `setFrame:` method.
|
||||
@public CGRect _frame;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation RCTScrollContentShadowView
|
||||
|
||||
- (void)applyLayoutNode:(YGNodeRef)node
|
||||
viewsWithNewFrame:(NSMutableSet<RCTShadowView *> *)viewsWithNewFrame
|
||||
absolutePosition:(CGPoint)absolutePosition
|
||||
{
|
||||
// Call super method if LTR layout is enforced.
|
||||
if (self.effectiveLayoutDirection == UIUserInterfaceLayoutDirectionLeftToRight) {
|
||||
[super applyLayoutNode:node
|
||||
viewsWithNewFrame:viewsWithNewFrame
|
||||
absolutePosition:absolutePosition];
|
||||
return;
|
||||
}
|
||||
|
||||
// Motivation:
|
||||
// Yoga place `contentView` on the right side of `scrollView` when RTL layout is enfoced.
|
||||
// That breaks everything; it is completly pointless to (re)position `contentView`
|
||||
// because it is `contentView`'s job. So, we work around it here.
|
||||
|
||||
// Step 1. Compensate `absolutePosition` change.
|
||||
CGFloat xCompensation = YGNodeLayoutGetRight(node) - YGNodeLayoutGetLeft(node);
|
||||
absolutePosition.x += xCompensation;
|
||||
|
||||
// Step 2. Call super method.
|
||||
[super applyLayoutNode:node
|
||||
viewsWithNewFrame:viewsWithNewFrame
|
||||
absolutePosition:absolutePosition];
|
||||
|
||||
// Step 3. Reset the position.
|
||||
_frame.origin.x = RCTRoundPixelValue(YGNodeLayoutGetRight(node));
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import <React/RCTViewManager.h>
|
||||
|
||||
@interface RCTScrollContentViewManager : RCTViewManager
|
||||
|
||||
@end
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
#import "RCTScrollContentViewManager.h"
|
||||
|
||||
#import "RCTScrollContentShadowView.h"
|
||||
|
||||
@implementation RCTScrollContentViewManager
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (RCTShadowView *)shadowView
|
||||
{
|
||||
return [RCTScrollContentShadowView new];
|
||||
}
|
||||
|
||||
@end
|
|
@ -159,6 +159,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||
{
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
[self.panGestureRecognizer addTarget:self action:@selector(handleCustomPan:)];
|
||||
|
||||
// We intentionaly force `UIScrollView`s `semanticContentAttribute` to `LTR` here
|
||||
// because this attribute affects a position of vertical scrollbar; we don't want this
|
||||
// scrollbar flip because we also flip it with whole `UIScrollView` flip.
|
||||
self.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -191,7 +196,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||
self.panGestureRecognizer.enabled = YES;
|
||||
// TODO: If mid bounce, animate the scroll view to a non-bounced position
|
||||
// while disabling (but only if `stopScrollInteractionIfJSHasResponder` was
|
||||
// called *during* a `pan`. Currently, it will just snap into place which
|
||||
// called *during* a `pan`). Currently, it will just snap into place which
|
||||
// is not so bad either.
|
||||
// Another approach:
|
||||
// self.scrollEnabled = NO;
|
||||
|
@ -278,9 +283,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|||
{
|
||||
UIView *contentView = [self contentView];
|
||||
CGFloat scrollTop = self.bounds.origin.y + self.contentInset.top;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// If the RefreshControl is refreshing, remove it's height so sticky headers are
|
||||
// positioned properly when scrolling down while refreshing.
|
||||
#if !TARGET_OS_TV
|
||||
if (_rctRefreshControl != nil && _rctRefreshControl.refreshing) {
|
||||
scrollTop -= _rctRefreshControl.frame.size.height;
|
||||
}
|
||||
|
@ -451,6 +457,21 @@ static inline BOOL isRectInvalid(CGRect rect) {
|
|||
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
|
||||
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
||||
|
||||
static inline void RCTApplyTranformationAccordingLayoutDirection(UIView *view, UIUserInterfaceLayoutDirection layoutDirection) {
|
||||
view.transform =
|
||||
layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ?
|
||||
CGAffineTransformIdentity :
|
||||
CGAffineTransformMakeScale(-1, 1);
|
||||
}
|
||||
|
||||
- (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
|
||||
{
|
||||
[super setReactLayoutDirection:layoutDirection];
|
||||
|
||||
RCTApplyTranformationAccordingLayoutDirection(_scrollView, layoutDirection);
|
||||
RCTApplyTranformationAccordingLayoutDirection(_contentView, layoutDirection);
|
||||
}
|
||||
|
||||
- (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
|
||||
{
|
||||
// Does nothing
|
||||
|
@ -467,6 +488,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
|
|||
{
|
||||
RCTAssert(_contentView == nil, @"RCTScrollView may only contain a single subview");
|
||||
_contentView = view;
|
||||
RCTApplyTranformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
|
||||
[_scrollView addSubview:view];
|
||||
}
|
||||
}
|
||||
|
@ -921,16 +943,9 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
|
|||
{
|
||||
if (!CGSizeEqualToSize(_contentSize, CGSizeZero)) {
|
||||
return _contentSize;
|
||||
} else if (!_contentView) {
|
||||
return CGSizeZero;
|
||||
} else {
|
||||
CGSize singleSubviewSize = _contentView.frame.size;
|
||||
CGPoint singleSubviewPosition = _contentView.frame.origin;
|
||||
return (CGSize){
|
||||
singleSubviewSize.width + singleSubviewPosition.x,
|
||||
singleSubviewSize.height + singleSubviewPosition.y
|
||||
};
|
||||
}
|
||||
|
||||
return _contentView.frame.size;
|
||||
}
|
||||
|
||||
- (void)reactBridgeDidFinishTransaction
|
||||
|
|
|
@ -62,6 +62,11 @@ typedef void (^RCTApplierBlock)(NSDictionary<NSNumber *, UIView *> *viewRegistry
|
|||
*/
|
||||
@property (nonatomic, assign, getter=isHidden) BOOL hidden;
|
||||
|
||||
/**
|
||||
* Computed layout direction for the view backed to Yoga node value.
|
||||
*/
|
||||
@property (nonatomic, assign, readonly) UIUserInterfaceLayoutDirection effectiveLayoutDirection;
|
||||
|
||||
/**
|
||||
* Position and dimensions.
|
||||
* Defaults to { 0, 0, NAN, NAN }.
|
||||
|
|
|
@ -438,6 +438,12 @@ static void RCTProcessMetaPropsBorder(const YGValue metaProps[META_PROP_COUNT],
|
|||
return description;
|
||||
}
|
||||
|
||||
// Layout Direction
|
||||
|
||||
- (UIUserInterfaceLayoutDirection)effectiveLayoutDirection {
|
||||
return YGNodeLayoutGetDirection(self.cssNode) == YGDirectionRTL ? UIUserInterfaceLayoutDirectionRightToLeft : UIUserInterfaceLayoutDirectionLeftToRight;
|
||||
}
|
||||
|
||||
// Margin
|
||||
|
||||
#define RCT_MARGIN_PROPERTY(prop, metaProp) \
|
||||
|
|
|
@ -40,6 +40,13 @@
|
|||
*/
|
||||
+ (UIEdgeInsets)contentInsetsForView:(UIView *)curView;
|
||||
|
||||
/**
|
||||
* Layout direction of the view.
|
||||
* This is inherited from UIView+React, but we override it here
|
||||
* to improve perfomance and make subclassing/overriding possible/easier.
|
||||
*/
|
||||
@property (nonatomic, assign) UIUserInterfaceLayoutDirection reactLayoutDirection;
|
||||
|
||||
/**
|
||||
* z-index, used to override sibling order in didUpdateReactSubviews. This is
|
||||
* inherited from UIView+React, but we override it here to reduce the boxing
|
||||
|
|
|
@ -124,6 +124,18 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
|
|||
|
||||
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
||||
|
||||
- (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
|
||||
{
|
||||
_reactLayoutDirection = layoutDirection;
|
||||
|
||||
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_9_0
|
||||
self.semanticContentAttribute =
|
||||
layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ?
|
||||
UISemanticContentAttributeForceLeftToRight :
|
||||
UISemanticContentAttributeForceRightToLeft;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSString *)accessibilityLabel
|
||||
{
|
||||
if (super.accessibilityLabel) {
|
||||
|
|
|
@ -23,6 +23,13 @@
|
|||
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER;
|
||||
- (void)removeReactSubview:(UIView *)subview NS_REQUIRES_SUPER;
|
||||
|
||||
/**
|
||||
* Layout direction of the view.
|
||||
* Internally backed to `semanticContentAttribute` property.
|
||||
* Defaults to `LeftToRight` in case of ambiguity.
|
||||
*/
|
||||
@property (nonatomic, assign) UIUserInterfaceLayoutDirection reactLayoutDirection;
|
||||
|
||||
/**
|
||||
* z-index, used to override sibling order in didUpdateReactSubviews.
|
||||
*/
|
||||
|
|
|
@ -87,6 +87,27 @@
|
|||
[subview removeFromSuperview];
|
||||
}
|
||||
|
||||
- (UIUserInterfaceLayoutDirection)reactLayoutDirection
|
||||
{
|
||||
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_9_0
|
||||
return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.semanticContentAttribute];
|
||||
#else
|
||||
return [objc_getAssociatedObject(self, @selector(reactLayoutDirection)) integerValue];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
|
||||
{
|
||||
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_9_0
|
||||
self.semanticContentAttribute =
|
||||
layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ?
|
||||
UISemanticContentAttributeForceLeftToRight :
|
||||
UISemanticContentAttributeForceRightToLeft;
|
||||
#else
|
||||
objc_setAssociatedObject(self, @selector(reactLayoutDirection), @(layoutDirection), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSInteger)reactZIndex
|
||||
{
|
||||
return [objc_getAssociatedObject(self, _cmd) integerValue];
|
||||
|
|
Loading…
Reference in New Issue