amis-rpc-design/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm

268 lines
9.2 KiB
Plaintext
Raw Normal View History

2023-10-07 19:42:30 +08:00
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTEnhancedScrollView.h"
#import <React/RCTUtils.h>
@interface RCTEnhancedScrollView () <UIScrollViewDelegate>
@end
@implementation RCTEnhancedScrollView {
__weak id<UIScrollViewDelegate> _publicDelegate;
BOOL _isSetContentOffsetDisabled;
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"delegate"]) {
// For `delegate` property, we issue KVO notifications manually.
// We need that to block notifications caused by setting the original `UIScrollView`s property.
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// We set the default behavior to "never" so that iOS
// doesn't do weird things to UIScrollView insets automatically
// and keeps it as an opt-in behavior.
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
// We intentionally 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;
__weak __typeof(self) weakSelf = self;
_delegateSplitter = [[RCTGenericDelegateSplitter alloc] initWithDelegateUpdateBlock:^(id delegate) {
[weakSelf setPrivateDelegate:delegate];
}];
[_delegateSplitter addDelegate:self];
}
return self;
}
- (void)preserveContentOffsetWithBlock:(void (^)())block
{
if (!block) {
return;
}
_isSetContentOffsetDisabled = YES;
block();
_isSetContentOffsetDisabled = NO;
}
/*
* Automatically centers the content such that if the content is smaller than the
* ScrollView, we force it to be centered, but when you zoom or the content otherwise
* becomes larger than the ScrollView, there is no padding around the content but it
* can still fill the whole view.
*/
- (void)setContentOffset:(CGPoint)contentOffset
{
if (_isSetContentOffsetDisabled) {
return;
}
if (_centerContent && !CGSizeEqualToSize(self.contentSize, CGSizeZero)) {
CGSize scrollViewSize = self.bounds.size;
if (self.contentSize.width <= scrollViewSize.width) {
contentOffset.x = -(scrollViewSize.width - self.contentSize.width) / 2.0;
}
if (self.contentSize.height <= scrollViewSize.height) {
contentOffset.y = -(scrollViewSize.height - self.contentSize.height) / 2.0;
}
}
super.contentOffset = CGPointMake(
RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
if ([_overridingDelegate respondsToSelector:@selector(touchesShouldCancelInContentView:)]) {
return [_overridingDelegate touchesShouldCancelInContentView:view];
}
return [super touchesShouldCancelInContentView:view];
}
#pragma mark - RCTGenericDelegateSplitter
- (void)setPrivateDelegate:(id<UIScrollViewDelegate>)delegate
{
[super setDelegate:delegate];
}
- (id<UIScrollViewDelegate>)delegate
{
return _publicDelegate;
}
- (void)setDelegate:(id<UIScrollViewDelegate>)delegate
{
if (_publicDelegate == delegate) {
return;
}
if (_publicDelegate) {
[_delegateSplitter removeDelegate:_publicDelegate];
}
[self willChangeValueForKey:@"delegate"];
_publicDelegate = delegate;
[self didChangeValueForKey:@"delegate"];
if (_publicDelegate) {
[_delegateSplitter addDelegate:_publicDelegate];
}
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
{
if (self.snapToOffsets && self.snapToOffsets.count > 0) {
// An alternative to enablePaging and snapToInterval which allows setting custom
// stopping points that don't have to be the same distance apart. Often seen in
// apps which feature horizonally scrolling items. snapToInterval does not enforce
// scrolling one interval at a time but guarantees that the scroll will stop at
// a snap offset point.
// Find which axis to snap
BOOL isHorizontal = [self isHorizontal:scrollView];
CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
CGFloat offsetAlongAxis = isHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y;
// Calculate maximum content offset
CGSize viewportSize = self.bounds.size;
CGFloat maximumOffset = isHorizontal ? MAX(0, scrollView.contentSize.width - viewportSize.width)
: MAX(0, scrollView.contentSize.height - viewportSize.height);
// Calculate the snap offsets adjacent to the initial offset target
CGFloat targetOffset = isHorizontal ? targetContentOffset->x : targetContentOffset->y;
CGFloat smallerOffset = 0.0;
CGFloat largerOffset = maximumOffset;
for (unsigned long i = 0; i < self.snapToOffsets.count; i++) {
CGFloat offset = [[self.snapToOffsets objectAtIndex:i] floatValue];
if (offset <= targetOffset) {
if (targetOffset - offset < targetOffset - smallerOffset) {
smallerOffset = offset;
}
}
if (offset >= targetOffset) {
if (offset - targetOffset < largerOffset - targetOffset) {
largerOffset = offset;
}
}
}
// Calculate the nearest offset
CGFloat nearestOffset = targetOffset - smallerOffset < largerOffset - targetOffset ? smallerOffset : largerOffset;
CGFloat firstOffset = [[self.snapToOffsets firstObject] floatValue];
CGFloat lastOffset = [[self.snapToOffsets lastObject] floatValue];
// if scrolling after the last snap offset and snapping to the
// end of the list is disabled, then we allow free scrolling
if (!self.snapToEnd && targetOffset >= lastOffset) {
if (offsetAlongAxis >= lastOffset) {
// free scrolling
} else {
// snap to end
targetOffset = lastOffset;
}
} else if (!self.snapToStart && targetOffset <= firstOffset) {
if (offsetAlongAxis <= firstOffset) {
// free scrolling
} else {
// snap to beginning
targetOffset = firstOffset;
}
} else if (velocityAlongAxis > 0.0) {
targetOffset = largerOffset;
} else if (velocityAlongAxis < 0.0) {
targetOffset = smallerOffset;
} else {
targetOffset = nearestOffset;
}
// Make sure the new offset isn't out of bounds
targetOffset = MIN(MAX(0, targetOffset), maximumOffset);
// Set new targetContentOffset
if (isHorizontal) {
targetContentOffset->x = targetOffset;
} else {
targetContentOffset->y = targetOffset;
}
} else if (self.snapToInterval) {
// An alternative to enablePaging which allows setting custom stopping intervals,
// smaller than a full page size. Often seen in apps which feature horizonally
// scrolling items. snapToInterval does not enforce scrolling one interval at a time
// but guarantees that the scroll will stop at an interval point.
CGFloat snapToIntervalF = (CGFloat)self.snapToInterval;
// Find which axis to snap
BOOL isHorizontal = [self isHorizontal:scrollView];
// What is the current offset?
CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
CGFloat targetContentOffsetAlongAxis = targetContentOffset->y;
if (isHorizontal) {
// Use current scroll offset to determine the next index to snap to when momentum disabled
targetContentOffsetAlongAxis = self.disableIntervalMomentum ? scrollView.contentOffset.x : targetContentOffset->x;
} else {
targetContentOffsetAlongAxis = self.disableIntervalMomentum ? scrollView.contentOffset.y : targetContentOffset->y;
}
// Offset based on desired alignment
CGFloat frameLength = isHorizontal ? self.frame.size.width : self.frame.size.height;
CGFloat alignmentOffset = 0.0f;
if ([self.snapToAlignment isEqualToString:@"center"]) {
alignmentOffset = (frameLength * 0.5f) + (snapToIntervalF * 0.5f);
} else if ([self.snapToAlignment isEqualToString:@"end"]) {
alignmentOffset = frameLength;
}
// Pick snap point based on direction and proximity
CGFloat fractionalIndex = (targetContentOffsetAlongAxis + alignmentOffset) / snapToIntervalF;
NSInteger snapIndex = velocityAlongAxis > 0.0 ? ceil(fractionalIndex)
: velocityAlongAxis < 0.0 ? floor(fractionalIndex)
: round(fractionalIndex);
CGFloat newTargetContentOffset = ((CGFloat)snapIndex * snapToIntervalF) - alignmentOffset;
// Set new targetContentOffset
if (isHorizontal) {
targetContentOffset->x = newTargetContentOffset;
} else {
targetContentOffset->y = newTargetContentOffset;
}
}
}
#pragma mark -
- (BOOL)isHorizontal:(UIScrollView *)scrollView
{
return scrollView.contentSize.width > self.frame.size.width;
}
@end