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;
+ (NSNumber *)NSNumber:(id)json;
+ (NSData *)NSData:(id)json;
+ (NSIndexSet *)NSIndexSet:(id)json;
+ (NSURL *)NSURL:(id)json;
+ (NSURLRequest *)NSURLRequest:(id)json;

View File

@ -64,6 +64,20 @@ RCT_CONVERTER(NSString *, NSString, description)
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
{
NSString *path = [self NSString:json];

View File

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

View File

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

View File

@ -28,8 +28,8 @@ CGFloat const ZINDEX_STICKY_HEADER = 50;
*/
@interface RCTCustomScrollView : UIScrollView<UIGestureRecognizerDelegate>
@property (nonatomic, copy, readwrite) NSArray *stickyHeaderIndices;
@property (nonatomic, readwrite, assign) BOOL centerContent;
@property (nonatomic, copy) NSIndexSet *stickyHeaderIndices;
@property (nonatomic, assign) BOOL centerContent;
@end
@ -155,97 +155,72 @@ CGFloat const ZINDEX_STICKY_HEADER = 50;
[super setContentOffset:contentOffset];
}
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
[self dockClosestSectionHeader];
}
- (void)dockClosestSectionHeader
{
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;
}
// find the section header that needs to be docked
NSInteger firstIndexInView = [[_stickyHeaderIndices firstObject] integerValue] + 1;
CGRect scrollBounds = self.bounds;
scrollBounds.origin.x += self.contentInset.left;
scrollBounds.origin.y += self.contentInset.top;
NSInteger i = 0;
for (UIView *subview in contentView.reactSubviews) {
CGRect rowFrame = [RCTCustomScrollView _calculateUntransformedFrame:subview];
if (CGRectIntersectsRect(scrollBounds, rowFrame)) {
firstIndexInView = i;
break;
}
i++;
// Adjust current header to hug the top of the screen
CGFloat currentFrameHeight = currentHeader.bounds.size.height;
CGFloat currentFrameTop = currentHeader.center.y - currentFrameHeight * currentHeader.layer.anchorPoint.y;
CGFloat yOffset = scrollTop - currentFrameTop;
if (nextHeader) {
// The next header nudges the current header out of the way when it reaches
// the top of the screen
CGFloat nextFrameHeight = nextHeader.bounds.size.height;
CGFloat nextFrameTop = nextHeader.center.y - nextFrameHeight * nextHeader.layer.anchorPoint.y;
CGFloat overlap = currentFrameHeight - (nextFrameTop - scrollTop);
yOffset -= MAX(0, overlap);
}
NSInteger stickyHeaderii = 0;
for (NSNumber *stickyHeaderI in _stickyHeaderIndices) {
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];
currentHeader.transform = CGAffineTransformMakeTranslation(0, yOffset);
currentHeader.layer.zPosition = ZINDEX_STICKY_HEADER;
if (previousHeader) {
// the previous header is offset to sit right above the currentlyDockedHeader's initial position
// (so it scrolls away nicely once the currentHeader locks into position)
CGRect previousFrame = [RCTCustomScrollView _calculateUntransformedFrame:previousHeader];
CGFloat yOffset = curFrame.origin.y - previousFrame.origin.y - previousFrame.size.height;
// The previous header sits right above the currentHeader's initial position
// so it scrolls away nicely once the currentHeader has locked into place
CGFloat previousFrameHeight = previousHeader.bounds.size.height;
CGFloat targetCenter = currentFrameTop - previousFrameHeight * (1.0 - previousHeader.layer.anchorPoint.y);
yOffset = targetCenter - previousHeader.center.y;
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
@ -312,7 +287,7 @@ CGFloat const ZINDEX_STICKY_HEADER = 50;
_scrollView.centerContent = centerContent;
}
- (void)setStickyHeaderIndices:(NSArray *)headerIndices
- (void)setStickyHeaderIndices:(NSIndexSet *)headerIndices
{
RCTAssert(_scrollView.contentSize.width <= self.frame.size.width,
@"sticky headers are not supported with horizontal scrolled views");
@ -340,14 +315,8 @@ CGFloat const ZINDEX_STICKY_HEADER = 50;
- (void)setContentInset:(UIEdgeInsets)contentInset
{
CGPoint contentOffset = _scrollView.contentOffset;
_contentInset = contentInset;
[RCTView autoAdjustInsetsForView:self
withScrollView:_scrollView
updateOffset:NO];
_scrollView.contentOffset = contentOffset;
[self setNeedsLayout];
}
- (void)scrollToOffset:(CGPoint)offset
@ -390,6 +359,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove)
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[_scrollView dockClosestSectionHeader];
[self updateClippedSubviews];
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(showsHorizontalScrollIndicator, 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(zoomScale, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)