Unforked RKNavigator, RKScrollView and RKView

This commit is contained in:
Nick Lockwood
2015-03-10 19:03:59 -07:00
parent 378e59b90a
commit 74e84ba494
14 changed files with 418 additions and 140 deletions

View File

@@ -259,14 +259,14 @@ CGFloat const ZINDEX_STICKY_HEADER = 50;
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
{
if ((self = [super initWithFrame:CGRectZero])) {
_eventDispatcher = eventDispatcher;
_scrollView = [[RCTCustomScrollView alloc] initWithFrame:CGRectZero];
_scrollView.delegate = self;
_scrollView.delaysContentTouches = NO;
_automaticallyAdjustContentInsets = YES;
_contentInset = UIEdgeInsetsZero;
_throttleScrollCallbackMS = 0;
_lastScrollDispatchTime = CACurrentMediaTime();
_cachedChildFrames = [[NSMutableArray alloc] init];
@@ -337,6 +337,8 @@ CGFloat const ZINDEX_STICKY_HEADER = 50;
[RCTView autoAdjustInsetsForView:self
withScrollView:_scrollView
updateOffset:YES];
[self updateClippedSubviews];
}
- (void)setContentInset:(UIEdgeInsets)contentInset
@@ -387,9 +389,11 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove)
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self updateClippedSubviews];
NSTimeInterval now = CACurrentMediaTime();
NSTimeInterval throttleScrollCallbackSeconds = _throttleScrollCallbackMS / 1000.0;
/**
* TODO: this logic looks wrong, and it may be because it is. Currently, if _throttleScrollCallbackMS
* is set to zero (the default), the "didScroll" event is only sent once per scroll, instead of repeatedly
@@ -398,11 +402,11 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove)
*/
if (_allowNextScrollNoMatterWhat ||
(_throttleScrollCallbackMS != 0 && throttleScrollCallbackSeconds < (now - _lastScrollDispatchTime))) {
// Calculate changed frames
NSMutableArray *updatedChildFrames = [[NSMutableArray alloc] init];
[[_contentView reactSubviews] enumerateObjectsUsingBlock:^(UIView *subview, NSUInteger idx, BOOL *stop) {
// Check if new or changed
CGRect newFrame = subview.frame;
BOOL frameChanged = NO;
@@ -413,7 +417,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove)
frameChanged = YES;
_cachedChildFrames[idx] = [NSValue valueWithCGRect:newFrame];
}
// Create JS frame object
if (frameChanged) {
[updatedChildFrames addObject: @{
@@ -424,9 +428,9 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove)
@"height": @(newFrame.size.height),
}];
}
}];
// If there are new frames, add them to event data
NSDictionary *userData = nil;
if (updatedChildFrames.count > 0) {
@@ -568,13 +572,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove)
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ([super respondsToSelector:aSelector]) {
return YES;
}
if ([NSStringFromSelector(aSelector) hasPrefix:@"set"]) {
return [_scrollView respondsToSelector:aSelector];
}
return NO;
return [super respondsToSelector:aSelector] || [_scrollView respondsToSelector:aSelector];
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key

View File

@@ -5,6 +5,8 @@
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTScrollView.h"
#import "RCTSparseArray.h"
#import "RCTUIManager.h"
@implementation RCTScrollViewManager
@@ -52,4 +54,25 @@ RCT_EXPORT_VIEW_PROPERTY(contentOffset);
};
}
- (void)getContentSize:(NSNumber *)reactTag
callback:(RCTResponseSenderBlock)callback
{
RCT_EXPORT();
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
UIView *view = viewRegistry[reactTag];
if (!view) {
RCTLogError(@"Cannot find view with tag %@", reactTag);
return;
}
CGSize size = ((id<RCTScrollableProtocol>)view).contentSize;
callback(@[@{
@"width" : @(size.width),
@"height" : @(size.height)
}]);
}];
}
@end

View File

@@ -8,7 +8,7 @@
*/
@protocol RCTScrollableProtocol
@property (nonatomic, readwrite, weak) NSObject<UIScrollViewDelegate> *nativeMainScrollDelegate;
@property (nonatomic, weak) NSObject<UIScrollViewDelegate> *nativeMainScrollDelegate;
@property (nonatomic, readonly) CGSize contentSize;
- (void)scrollToOffset:(CGPoint)offset;

View File

@@ -99,7 +99,6 @@
// we can't hook up the VC hierarchy in 'init' because the subviews aren't
// hooked up yet, so we do it on demand here whenever a transaction has finished
[self addControllerToClosestParent:_tabController];
//[RCTView addViewController:_tabController toBackingViewControllerForView:self];
if (_tabsChanged) {

View File

@@ -24,4 +24,21 @@
*/
+ (UIEdgeInsets)contentInsetsForView:(UIView *)curView;
/**
* This is an optimization used to improve performance
* for large scrolling views with many subviews, such as a
* list or table. If set to YES, any clipped subviews will
* be removed from the view hierarchy whenever -updateClippedSubviews
* is called. This would typically be triggered by a scroll event
*/
@property (nonatomic, assign) BOOL removeClippedSubviews;
/**
* Hide subviews if they are outside the view bounds.
* This is an optimisation used predominantly with RKScrollViews
* but it is applied recursively to all subviews that have
* removeClippedSubviews set to YES
*/
- (void)updateClippedSubviews;
@end

View File

@@ -7,6 +7,65 @@
#import "RCTLog.h"
#import "UIView+ReactKit.h"
@implementation UIView (RCTViewUnmounting)
- (void)react_remountAllSubviews
{
// Normal views don't support unmounting, so all
// this does is forward message to our subviews,
// in case any of those do support it
for (UIView *subview in self.subviews) {
[subview react_remountAllSubviews];
}
}
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
{
// Even though we don't support subview unmounting
// we do support clipsToBounds, so if that's enabled
// we'll update the clipping
if (self.clipsToBounds && [self.subviews count] > 0) {
clipRect = [clipView convertRect:clipRect toView:self];
clipRect = CGRectIntersection(clipRect, self.bounds);
clipView = self;
}
// Normal views don't support unmounting, so all
// this does is forward message to our subviews,
// in case any of those do support it
for (UIView *subview in self.subviews) {
[subview react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
}
}
- (UIView *)react_findClipView
{
UIView *testView = self;
UIView *clipView = nil;
CGRect clipRect = self.bounds;
while (testView) {
if (testView.clipsToBounds) {
if (clipView) {
CGRect testRect = [clipView convertRect:clipRect toView:testView];
if (!CGRectContainsRect(testView.bounds, testRect)) {
clipView = testView;
clipRect = CGRectIntersection(testView.bounds, testRect);
}
} else {
clipView = testView;
clipRect = [self convertRect:self.bounds toView:clipView];
}
}
testView = testView.superview;
}
return clipView ?: self.window;
}
@end
static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
{
NSMutableString *str = [NSMutableString stringWithString:@""];
@@ -23,6 +82,9 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
}
@implementation RCTView
{
NSMutableArray *_reactSubviews;
}
- (NSString *)accessibilityLabel
{
@@ -115,4 +177,193 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
return UIEdgeInsetsZero;
}
#pragma mark - View unmounting
- (void)react_remountAllSubviews
{
if (_reactSubviews) {
NSInteger index = 0;
for (UIView *view in _reactSubviews) {
if (view.superview != self) {
if (index < [self subviews].count) {
[self insertSubview:view atIndex:index];
} else {
[self addSubview:view];
}
[view react_remountAllSubviews];
}
index++;
}
} else {
// If react_subviews is nil, we must already be showing all subviews
[super react_remountAllSubviews];
}
}
- (void)remountSubview:(UIView *)view
{
// Calculate insertion index for view
NSInteger index = 0;
for (UIView *subview in _reactSubviews) {
if (subview == view) {
[self insertSubview:view atIndex:index];
break;
}
if (subview.superview) {
// View is mounted, so bump the index
index++;
}
}
}
- (void)mountOrUnmountSubview:(UIView *)view withClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
{
if (view.clipsToBounds) {
// View has cliping enabled, so we can easily test if it is partially
// or completely within the clipRect, and mount or unmount it accordingly
if (CGRectIntersectsRect(clipRect, view.frame)) {
// View is at least partially visible, so remount it if unmounted
if (view.superview == nil) {
[self remountSubview:view];
}
// Then test its subviews
if (CGRectContainsRect(clipRect, view.frame)) {
[view react_remountAllSubviews];
} else {
[view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
}
} else if (view.superview) {
// View is completely outside the clipRect, so unmount it
[view removeFromSuperview];
}
} else {
// View has clipping disabled, so there's no way to tell if it has
// any visible subviews without an expensive recursive test, so we'll
// just add it.
if (view.superview == nil) {
[self remountSubview:view];
}
// Check if subviews need to be mounted/unmounted
[view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
}
}
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
{
// TODO (#5906496): for scrollviews (the primary use-case) we could
// optimize this by only doing a range check along the scroll axis,
// instead of comparing the whole frame
if (_reactSubviews == nil) {
// Use default behavior if unmounting is disabled
return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
}
if ([_reactSubviews count] == 0) {
// Do nothing if we have no subviews
return;
}
if (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
// Do nothing if layout hasn't happened yet
return;
}
// Convert clipping rect to local coordinates
clipRect = [clipView convertRect:clipRect toView:self];
clipView = self;
if (self.clipsToBounds) {
clipRect = CGRectIntersection(clipRect, self.bounds);
}
// Mount / unmount views
for (UIView *view in _reactSubviews) {
[self mountOrUnmountSubview:view withClipRect:clipRect relativeToView:clipView];
}
}
- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews
{
if (removeClippedSubviews && !_reactSubviews) {
_reactSubviews = [self.subviews mutableCopy];
} else if (!removeClippedSubviews && _reactSubviews) {
[self react_remountAllSubviews];
_reactSubviews = nil;
}
}
- (BOOL)removeClippedSubviews
{
return _reactSubviews != nil;
}
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
{
if (_reactSubviews == nil) {
[self insertSubview:view atIndex:atIndex];
} else {
[_reactSubviews insertObject:view atIndex:atIndex];
// Find a suitable view to use for clipping
UIView *clipView = [self react_findClipView];
if (clipView) {
// If possible, don't add subviews if they are clipped
[self mountOrUnmountSubview:view withClipRect:clipView.bounds relativeToView:clipView];
} else {
// Fallback if we can't find a suitable clipView
[self remountSubview:view];
}
}
}
- (void)removeReactSubview:(UIView *)subview
{
[_reactSubviews removeObject:subview];
[subview removeFromSuperview];
}
- (NSArray *)reactSubviews
{
// The _reactSubviews array is only used when we have hidden
// offscreen views. If _reactSubviews is nil, we can assume
// that [self reactSubviews] and [self subviews] are the same
return _reactSubviews ?: [self subviews];
}
- (void)updateClippedSubviews
{
// Find a suitable view to use for clipping
UIView *clipView = [self react_findClipView];
if (clipView) {
[self react_updateClippedSubviewsWithClipRect:clipView.bounds relativeToView:clipView];
}
}
- (void)layoutSubviews
{
// TODO (#5906496): this a nasty performance drain, but necessary
// to prevent gaps appearing when the loading spinner disappears.
// We might be able to fix this another way by triggering a call
// to updateClippedSubviews manually after loading
[super layoutSubviews];
if (_reactSubviews) {
[self updateClippedSubviews];
}
}
@end

View File

@@ -99,8 +99,8 @@
- (void)loadView
{
// add a wrapper so that UIViewControllerWrapperView (managed by the
// UINavigationController) doesn't end up resetting the frames for
// Add a wrapper so that the wrapper view managed by the
// UINavigationController doesn't end up resetting the frames for
//`contentView` which is a react-managed view.
_wrapperView = [[UIView alloc] initWithFrame:_contentView.bounds];
[_wrapperView addSubview:_contentView];