Fixed broken listview header alignment

This commit is contained in:
Nick Lockwood 2015-05-06 10:46:45 -07:00
parent 8c95de11e1
commit 790cee6e26
6 changed files with 79 additions and 94 deletions

View File

@ -39,6 +39,7 @@
+ (NSString *)NSString:(id)json; + (NSString *)NSString:(id)json;
+ (NSNumber *)NSNumber:(id)json; + (NSNumber *)NSNumber:(id)json;
+ (NSData *)NSData:(id)json; + (NSData *)NSData:(id)json;
+ (NSIndexSet *)NSIndexSet:(id)json;
+ (NSURL *)NSURL:(id)json; + (NSURL *)NSURL:(id)json;
+ (NSURLRequest *)NSURLRequest:(id)json; + (NSURLRequest *)NSURLRequest:(id)json;

View File

@ -64,6 +64,20 @@ RCT_CONVERTER(NSString *, NSString, description)
return [[self NSString:json] dataUsingEncoding:NSUTF8StringEncoding]; return [[self NSString:json] dataUsingEncoding:NSUTF8StringEncoding];
} }
+ (NSIndexSet *)NSIndexSet:(id)json
{
json = [self NSNumberArray:json];
NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init];
for (NSNumber *number in json) {
NSInteger index = number.integerValue;
if (RCT_DEBUG && index < 0) {
RCTLogError(@"Invalid index value %zd. Indices must be positive.", index);
}
[indexSet addIndex:index];
}
return indexSet;
}
+ (NSURL *)NSURL:(id)json + (NSURL *)NSURL:(id)json
{ {
NSString *path = [self NSString:json]; NSString *path = [self NSString:json];

View File

@ -119,8 +119,6 @@ RCT_EXPORT_MODULE()
- (void)updateSettings - (void)updateSettings
{ {
_settings = [NSMutableDictionary dictionaryWithDictionary:[_defaults objectForKey:RCTDevMenuSettingsKey]];
__weak RCTDevMenu *weakSelf = self; __weak RCTDevMenu *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
RCTDevMenu *strongSelf = weakSelf; RCTDevMenu *strongSelf = weakSelf;
@ -128,6 +126,8 @@ RCT_EXPORT_MODULE()
return; return;
} }
strongSelf->_settings = [NSMutableDictionary dictionaryWithDictionary:[strongSelf->_defaults objectForKey:RCTDevMenuSettingsKey]];
strongSelf.shakeToShow = [strongSelf->_settings[@"shakeToShow"] ?: @YES boolValue]; strongSelf.shakeToShow = [strongSelf->_settings[@"shakeToShow"] ?: @YES boolValue];
strongSelf.profilingEnabled = [strongSelf->_settings[@"profilingEnabled"] ?: @NO boolValue]; strongSelf.profilingEnabled = [strongSelf->_settings[@"profilingEnabled"] ?: @NO boolValue];
strongSelf.liveReloadEnabled = [strongSelf->_settings[@"liveReloadEnabled"] ?: @NO boolValue]; strongSelf.liveReloadEnabled = [strongSelf->_settings[@"liveReloadEnabled"] ?: @NO boolValue];

View File

@ -45,6 +45,6 @@
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
@property (nonatomic, assign) NSTimeInterval scrollEventThrottle; @property (nonatomic, assign) NSTimeInterval scrollEventThrottle;
@property (nonatomic, assign) BOOL centerContent; @property (nonatomic, assign) BOOL centerContent;
@property (nonatomic, copy) NSArray *stickyHeaderIndices; @property (nonatomic, copy) NSIndexSet *stickyHeaderIndices;
@end @end

View File

@ -28,8 +28,8 @@ CGFloat const ZINDEX_STICKY_HEADER = 50;
*/ */
@interface RCTCustomScrollView : UIScrollView<UIGestureRecognizerDelegate> @interface RCTCustomScrollView : UIScrollView<UIGestureRecognizerDelegate>
@property (nonatomic, copy, readwrite) NSArray *stickyHeaderIndices; @property (nonatomic, copy) NSIndexSet *stickyHeaderIndices;
@property (nonatomic, readwrite, assign) BOOL centerContent; @property (nonatomic, assign) BOOL centerContent;
@end @end
@ -155,97 +155,72 @@ CGFloat const ZINDEX_STICKY_HEADER = 50;
[super setContentOffset:contentOffset]; [super setContentOffset:contentOffset];
} }
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
[self dockClosestSectionHeader];
}
- (void)dockClosestSectionHeader - (void)dockClosestSectionHeader
{ {
UIView *contentView = [self contentView]; UIView *contentView = [self contentView];
if (_stickyHeaderIndices.count == 0 || !contentView) { CGFloat scrollTop = self.bounds.origin.y + self.contentInset.top;
// Find the section headers that need to be docked
__block UIView *previousHeader = nil;
__block UIView *currentHeader = nil;
__block UIView *nextHeader = nil;
NSInteger subviewCount = contentView.reactSubviews.count;
[_stickyHeaderIndices enumerateIndexesWithOptions:0 usingBlock:^(NSUInteger idx, BOOL *stop) {
if (idx >= subviewCount) {
RCTLogError(@"Sticky header index %zd was outside the range {0, %zd}", idx, subviewCount);
return;
}
UIView *header = contentView.reactSubviews[idx];
// If nextHeader not yet found, search for docked headers
if (!nextHeader) {
CGFloat height = header.bounds.size.height;
CGFloat top = header.center.y - height * header.layer.anchorPoint.y;
if (top > scrollTop) {
nextHeader = header;
} else {
previousHeader = currentHeader;
currentHeader = header;
}
}
// Reset transforms for header views
header.transform = CGAffineTransformIdentity;
header.layer.zPosition = ZINDEX_DEFAULT;
}];
// If no docked header, bail out
if (!currentHeader) {
return; return;
} }
// find the section header that needs to be docked // Adjust current header to hug the top of the screen
NSInteger firstIndexInView = [[_stickyHeaderIndices firstObject] integerValue] + 1; CGFloat currentFrameHeight = currentHeader.bounds.size.height;
CGRect scrollBounds = self.bounds; CGFloat currentFrameTop = currentHeader.center.y - currentFrameHeight * currentHeader.layer.anchorPoint.y;
scrollBounds.origin.x += self.contentInset.left; CGFloat yOffset = scrollTop - currentFrameTop;
scrollBounds.origin.y += self.contentInset.top; if (nextHeader) {
// The next header nudges the current header out of the way when it reaches
NSInteger i = 0; // the top of the screen
for (UIView *subview in contentView.reactSubviews) { CGFloat nextFrameHeight = nextHeader.bounds.size.height;
CGRect rowFrame = [RCTCustomScrollView _calculateUntransformedFrame:subview]; CGFloat nextFrameTop = nextHeader.center.y - nextFrameHeight * nextHeader.layer.anchorPoint.y;
if (CGRectIntersectsRect(scrollBounds, rowFrame)) { CGFloat overlap = currentFrameHeight - (nextFrameTop - scrollTop);
firstIndexInView = i; yOffset -= MAX(0, overlap);
break;
}
i++;
} }
NSInteger stickyHeaderii = 0; currentHeader.transform = CGAffineTransformMakeTranslation(0, yOffset);
for (NSNumber *stickyHeaderI in _stickyHeaderIndices) { currentHeader.layer.zPosition = ZINDEX_STICKY_HEADER;
if ([stickyHeaderI integerValue] > firstIndexInView) {
break;
}
stickyHeaderii++;
}
stickyHeaderii = MAX(0, stickyHeaderii - 1);
// Set up transforms for the various section headers
NSInteger currentlyDockedIndex = [_stickyHeaderIndices[stickyHeaderii] integerValue];
NSInteger previouslyDockedIndex = stickyHeaderii > 0 ? [_stickyHeaderIndices[stickyHeaderii-1] integerValue] : -1;
NSInteger nextDockedIndex = (stickyHeaderii < _stickyHeaderIndices.count - 1) ?
[_stickyHeaderIndices[stickyHeaderii + 1] integerValue] : -1;
UIView *currentHeader = contentView.reactSubviews[currentlyDockedIndex];
UIView *previousHeader = previouslyDockedIndex >= 0 ? contentView.reactSubviews[previouslyDockedIndex] : nil;
CGRect curFrame = [RCTCustomScrollView _calculateUntransformedFrame:currentHeader];
if (previousHeader) { if (previousHeader) {
// the previous header is offset to sit right above the currentlyDockedHeader's initial position // The previous header sits right above the currentHeader's initial position
// (so it scrolls away nicely once the currentHeader locks into position) // so it scrolls away nicely once the currentHeader has locked into place
CGRect previousFrame = [RCTCustomScrollView _calculateUntransformedFrame:previousHeader]; CGFloat previousFrameHeight = previousHeader.bounds.size.height;
CGFloat yOffset = curFrame.origin.y - previousFrame.origin.y - previousFrame.size.height; CGFloat targetCenter = currentFrameTop - previousFrameHeight * (1.0 - previousHeader.layer.anchorPoint.y);
yOffset = targetCenter - previousHeader.center.y;
previousHeader.transform = CGAffineTransformMakeTranslation(0, yOffset); previousHeader.transform = CGAffineTransformMakeTranslation(0, yOffset);
previousHeader.layer.zPosition = ZINDEX_STICKY_HEADER;
} }
UIView *nextHeader = nextDockedIndex >= 0 ? contentView.reactSubviews[nextDockedIndex] : nil;
CGRect nextFrame = [RCTCustomScrollView _calculateUntransformedFrame:nextHeader];
if (curFrame.origin.y < scrollBounds.origin.y) {
// scrolled off (or being scrolled off) the top of the screen
CGFloat yOffset = 0;
if (nextHeader && nextFrame.origin.y < scrollBounds.origin.y + curFrame.size.height) {
// next frame is bumping me off if scrolling down (or i'm bumping the next one off if scrolling up)
yOffset = nextFrame.origin.y - curFrame.origin.y - curFrame.size.height;
} else {
// standard sticky header position
yOffset = scrollBounds.origin.y - curFrame.origin.y;
}
currentHeader.transform = CGAffineTransformMakeTranslation(0, yOffset);
currentHeader.layer.zPosition = ZINDEX_STICKY_HEADER;
} else {
// i'm the current header but in the viewport, so just scroll in normal position
currentHeader.transform = CGAffineTransformIdentity;
currentHeader.layer.zPosition = ZINDEX_DEFAULT;
}
// in our setup, 'next header' will always just scroll with the page
if (nextHeader) {
nextHeader.transform = CGAffineTransformIdentity;
nextHeader.layer.zPosition = ZINDEX_DEFAULT;
}
}
+ (CGRect)_calculateUntransformedFrame:(UIView *)view
{
CGRect frame = CGRectNull;
if (view) {
frame.size = view.bounds.size;
frame.origin = CGPointMake(view.layer.position.x - view.bounds.size.width * view.layer.anchorPoint.x, view.layer.position.y - view.bounds.size.height * view.layer.anchorPoint.y);
}
return frame;
} }
@end @end
@ -312,7 +287,7 @@ CGFloat const ZINDEX_STICKY_HEADER = 50;
_scrollView.centerContent = centerContent; _scrollView.centerContent = centerContent;
} }
- (void)setStickyHeaderIndices:(NSArray *)headerIndices - (void)setStickyHeaderIndices:(NSIndexSet *)headerIndices
{ {
RCTAssert(_scrollView.contentSize.width <= self.frame.size.width, RCTAssert(_scrollView.contentSize.width <= self.frame.size.width,
@"sticky headers are not supported with horizontal scrolled views"); @"sticky headers are not supported with horizontal scrolled views");
@ -340,14 +315,8 @@ CGFloat const ZINDEX_STICKY_HEADER = 50;
- (void)setContentInset:(UIEdgeInsets)contentInset - (void)setContentInset:(UIEdgeInsets)contentInset
{ {
CGPoint contentOffset = _scrollView.contentOffset;
_contentInset = contentInset; _contentInset = contentInset;
[RCTView autoAdjustInsetsForView:self [self setNeedsLayout];
withScrollView:_scrollView
updateOffset:NO];
_scrollView.contentOffset = contentOffset;
} }
- (void)scrollToOffset:(CGPoint)offset - (void)scrollToOffset:(CGPoint)offset
@ -390,6 +359,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove)
- (void)scrollViewDidScroll:(UIScrollView *)scrollView - (void)scrollViewDidScroll:(UIScrollView *)scrollView
{ {
[_scrollView dockClosestSectionHeader];
[self updateClippedSubviews]; [self updateClippedSubviews];
NSTimeInterval now = CACurrentMediaTime(); NSTimeInterval now = CACurrentMediaTime();

View File

@ -41,7 +41,7 @@ RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(scrollsToTop, BOOL) RCT_EXPORT_VIEW_PROPERTY(scrollsToTop, BOOL)
RCT_EXPORT_VIEW_PROPERTY(showsHorizontalScrollIndicator, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsHorizontalScrollIndicator, BOOL)
RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL)
RCT_EXPORT_VIEW_PROPERTY(stickyHeaderIndices, NSNumberArray) RCT_EXPORT_VIEW_PROPERTY(stickyHeaderIndices, NSIndexSet)
RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval) RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval)
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat) RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)