|
|
|
@ -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();
|
|
|
|
|