From 1e5e5d3f9380f5f7c8a94cfb564bdddff1c44821 Mon Sep 17 00:00:00 2001 From: wwwcg Date: Mon, 9 Dec 2024 21:56:18 +0800 Subject: [PATCH 1/5] feat(ios): nested scroll api support --- .../listview/HippyNextBaseListViewManager.m | 6 + .../listview/HippyNextListTableView.h | 6 +- .../listview/HippyNextListTableView.m | 17 + .../scrollview/HippyNestedScrollCoordinator.h | 50 +++ .../scrollview/HippyNestedScrollCoordinator.m | 378 ++++++++++++++++++ .../component/scrollview/HippyScrollView.h | 43 +- .../component/scrollview/HippyScrollView.mm | 60 +++ .../scrollview/HippyScrollViewManager.mm | 7 + .../scrollview/HippyScrollableProtocol.h | 20 + .../waterfalllist/HippyWaterfallView.mm | 47 ++- .../waterfalllist/HippyWaterfallViewManager.m | 5 + .../ios/utils/HippyConvert+NativeRender.h | 16 + .../ios/utils/HippyConvert+NativeRender.m | 10 + 13 files changed, 657 insertions(+), 8 deletions(-) create mode 100644 renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.h create mode 100644 renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m diff --git a/renderer/native/ios/renderer/component/listview/HippyNextBaseListViewManager.m b/renderer/native/ios/renderer/component/listview/HippyNextBaseListViewManager.m index 48d57088841..9f7e97aa6a5 100644 --- a/renderer/native/ios/renderer/component/listview/HippyNextBaseListViewManager.m +++ b/renderer/native/ios/renderer/component/listview/HippyNextBaseListViewManager.m @@ -46,6 +46,12 @@ @implementation HippyNextBaseListViewManager HIPPY_EXPORT_VIEW_PROPERTY(showScrollIndicator, BOOL) HIPPY_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL) HIPPY_EXPORT_VIEW_PROPERTY(horizontal, BOOL) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollTopPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollLeftPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollBottomPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollRightPriority, HippyNestedScrollPriority) + - (UIView *)view { return [[HippyNextBaseListView alloc] init]; diff --git a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h index af83e54a19a..44afe84f6cb 100644 --- a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h +++ b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h @@ -21,6 +21,7 @@ */ #import +#import "HippyScrollView.h" NS_ASSUME_NONNULL_BEGIN @@ -35,8 +36,11 @@ NS_ASSUME_NONNULL_BEGIN @end -@interface HippyNextListTableView : UICollectionView +/// Custom tableView (collectionView) of Hippy +@interface HippyNextListTableView : UICollectionView + +/// Layout delegate @property (nonatomic, weak) id layoutDelegate; @end diff --git a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m index f4fd24790e5..b97e8384a64 100644 --- a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m +++ b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m @@ -21,9 +21,16 @@ */ #import "HippyNextListTableView.h" +#import "HippyScrollView.h" @implementation HippyNextListTableView +@synthesize lastContentOffset; +@synthesize activeInnerScrollView; +@synthesize activeOuterScrollView; +@synthesize nestedGestureDelegate; +@synthesize cascadeLockForNestedScroll; + /** * we need scroll indicator to be at top * indicator is UIImageView type at lower ios version @@ -45,4 +52,14 @@ - (void)layoutSubviews { } } +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer +shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + if (self.nestedGestureDelegate && + gestureRecognizer == self.panGestureRecognizer && + [self.nestedGestureDelegate respondsToSelector:@selector(shouldRecognizeScrollGestureSimultaneouslyWithView:)]) { + return [self.nestedGestureDelegate shouldRecognizeScrollGestureSimultaneouslyWithView:otherGestureRecognizer.view]; + } + return NO; +} + @end diff --git a/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.h b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.h new file mode 100644 index 00000000000..d5ea962ff64 --- /dev/null +++ b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.h @@ -0,0 +1,50 @@ +/*! + * iOS SDK + * + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import "HippyScrollView.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A coordinator responsible for managing scroll priorities +@interface HippyNestedScrollCoordinator : NSObject + +/// Priority of nestedScroll in all direction. +@property (nonatomic, assign) HippyNestedScrollPriority nestedScrollPriority; +/// Priority of nestedScroll in specific direction (finger move from bottom to top). +@property (nonatomic, assign) HippyNestedScrollPriority nestedScrollTopPriority; +/// Priority of nestedScroll in specific direction (finger move from right to left). +@property (nonatomic, assign) HippyNestedScrollPriority nestedScrollLeftPriority; +/// Priority of nestedScroll in specific direction (finger move from top to bottom). +@property (nonatomic, assign) HippyNestedScrollPriority nestedScrollBottomPriority; +/// Priority of nestedScroll in specific direction (finger move from left to right). +@property (nonatomic, assign) HippyNestedScrollPriority nestedScrollRightPriority; + +/// The inner scrollable view +@property (nonatomic, weak) UIScrollView *innerScrollView; +/// The outer scrollable view +@property (nonatomic, weak) UIScrollView *outerScrollView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m new file mode 100644 index 00000000000..121b1240325 --- /dev/null +++ b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m @@ -0,0 +1,378 @@ +/*! + * iOS SDK + * + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "HippyNestedScrollCoordinator.h" +#import "HippyAssert.h" +#import "HippyLog.h" + + +static NSString *const kHippyNestedScrollLog= @"NestedScroll"; +#define HippyNSLogTrace(...) HippyLogTrace(@"%@ %p %@", kHippyNestedScrollLog, self, [NSString stringWithFormat:__VA_ARGS__]) +#define HIPPY_NESTED_OPEN_BOUNCES 0 // Turn off the outer bounces feature for now + + +typedef NS_ENUM(char, HippyNestedScrollDirection) { + HippyNestedScrollDirectionNone = 0, + HippyNestedScrollDirectionLeft, + HippyNestedScrollDirectionRight, + HippyNestedScrollDirectionUp, + HippyNestedScrollDirectionDown, +}; + +typedef NS_ENUM(char, HippyNestedScrollDragType) { + HippyNestedScrollDragTypeUndefined = 0, + HippyNestedScrollDragTypeOuterOnly, + HippyNestedScrollDragTypeBoth, +}; + +static CGFloat const kHippyNestedScrollFloatThreshold = 0.1; + +@interface HippyNestedScrollCoordinator () + +/// Current drag type, used to judge the sliding order. +@property (nonatomic, assign) HippyNestedScrollDragType dragType; + +/// Whether should `unlock` the outerScrollView +/// One thing to note is the OuterScrollView may jitter in PrioritySelf mode since lock is a little bit late, +/// we need to make sure the initial state is NO to lock the outerScrollView. +@property (nonatomic, assign) BOOL shouldUnlockOuterScrollView; + +/// Whether should `unlock` the innerScrollView +@property (nonatomic, assign) BOOL shouldUnlockInnerScrollView; + +@end + + +@implementation HippyNestedScrollCoordinator + +- (void)setInnerScrollView:(UIScrollView *)innerScrollView { + HippyAssertParam(innerScrollView); + _innerScrollView = innerScrollView; + // Disable inner's bounces when nested scroll. + _innerScrollView.bounces = NO; +} + +- (void)setOuterScrollView:(UIScrollView *)outerScrollView { + _outerScrollView = outerScrollView; + _outerScrollView.bounces = NO; +} + +- (BOOL)isDirection:(HippyNestedScrollDirection)direction hasPriority:(HippyNestedScrollPriority)priority { + // Note that the top and bottom defined in the nestedScroll attribute refer to the finger orientation, + // as opposed to the page orientation. + HippyNestedScrollPriority presetPriority = HippyNestedScrollPriorityUndefined; + switch (direction) { + case HippyNestedScrollDirectionUp: + presetPriority = self.nestedScrollBottomPriority; + break; + case HippyNestedScrollDirectionDown: + presetPriority = self.nestedScrollTopPriority; + break; + case HippyNestedScrollDirectionLeft: + presetPriority = self.nestedScrollRightPriority; + break; + case HippyNestedScrollDirectionRight: + presetPriority = self.nestedScrollLeftPriority; + break; + default: + break; + } + if ((presetPriority == HippyNestedScrollPriorityUndefined) && + (self.nestedScrollPriority == HippyNestedScrollPriorityUndefined)) { + // Default value is `PrioritySelf`. + return (HippyNestedScrollPrioritySelf == priority); + } + return ((presetPriority == HippyNestedScrollPriorityUndefined) ? + (self.nestedScrollPriority == priority) : + (presetPriority == priority)); +} + +static inline BOOL hasScrollToTheDirectionEdge(const UIScrollView *scrollview, + const HippyNestedScrollDirection direction) { + if (HippyNestedScrollDirectionDown == direction) { + return ((scrollview.contentOffset.y + CGRectGetHeight(scrollview.frame)) + >= scrollview.contentSize.height - kHippyNestedScrollFloatThreshold); + } else if (HippyNestedScrollDirectionUp == direction) { + return scrollview.contentOffset.y <= kHippyNestedScrollFloatThreshold; + } else if (HippyNestedScrollDirectionLeft == direction) { + return scrollview.contentOffset.x <= kHippyNestedScrollFloatThreshold; + } else if (HippyNestedScrollDirectionRight == direction) { + return ((scrollview.contentOffset.x + CGRectGetWidth(scrollview.frame)) + >= scrollview.contentSize.width - kHippyNestedScrollFloatThreshold); + } + return NO; +} + +static inline BOOL isScrollInSpringbackState(const UIScrollView *scrollview, + const HippyNestedScrollDirection direction) { + if (HippyNestedScrollDirectionDown == direction) { + return scrollview.contentOffset.y <= -kHippyNestedScrollFloatThreshold; + } else if (HippyNestedScrollDirectionUp == direction) { + return (scrollview.contentOffset.y + CGRectGetHeight(scrollview.frame) + >= scrollview.contentSize.height + kHippyNestedScrollFloatThreshold); + } if (HippyNestedScrollDirectionLeft == direction) { + return scrollview.contentOffset.x <= -kHippyNestedScrollFloatThreshold; + } else if (HippyNestedScrollDirectionRight == direction) { + return (scrollview.contentOffset.x + CGRectGetWidth(scrollview.frame) + >= scrollview.contentSize.width - kHippyNestedScrollFloatThreshold); + } + return NO; +} + + +#pragma mark - ScrollEvents Delegate + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + const UIScrollView *sv = (UIScrollView *)scrollView; + const UIScrollView *outerScrollView = self.outerScrollView; + const UIScrollView *innerScrollView = self.innerScrollView; + BOOL isOuter = (sv == outerScrollView); + BOOL isInner = (sv == innerScrollView); + + HippyNSLogTrace(@"%@(%p) did scroll: %@", + isOuter ? @"Outer" : @"Inner", sv, + isOuter ? + NSStringFromCGPoint(outerScrollView.contentOffset) : + NSStringFromCGPoint(innerScrollView.contentOffset)); + + // 0. Exclude irrelevant scroll events using `activeInnerScrollView` + if (outerScrollView.activeInnerScrollView && + outerScrollView.activeInnerScrollView != innerScrollView) { + HippyNSLogTrace(@"Not active inner return."); + return; + } + + // 1. Determine direction of scrolling + HippyNestedScrollDirection direction = HippyNestedScrollDirectionNone; + if (sv.lastContentOffset.y > sv.contentOffset.y) { + direction = HippyNestedScrollDirectionUp; + } else if (sv.lastContentOffset.y < sv.contentOffset.y) { + direction = HippyNestedScrollDirectionDown; + } else if (sv.lastContentOffset.x > sv.contentOffset.x) { + direction = HippyNestedScrollDirectionLeft; + } else if (sv.lastContentOffset.x < sv.contentOffset.x) { + direction = HippyNestedScrollDirectionRight; + } + if (direction == HippyNestedScrollDirectionNone) { + HippyNSLogTrace(@"No direction return. %p", sv); + return; + } + + // 2. Lock inner scrollview if necessary + if ([self isDirection:direction hasPriority:HippyNestedScrollPriorityParent]) { + if (isOuter || (isInner && !self.shouldUnlockInnerScrollView)) { + if (hasScrollToTheDirectionEdge(outerScrollView, direction)) { + // Outer has slipped to the edge, + // need to further determine whether the Inner can still slide + if (hasScrollToTheDirectionEdge(innerScrollView, direction)) { + self.shouldUnlockInnerScrollView = NO; + HippyNSLogTrace(@"set lock inner !"); + } else { + self.shouldUnlockInnerScrollView = YES; + HippyNSLogTrace(@"set unlock inner ~"); + } + } else { + self.shouldUnlockInnerScrollView = NO; + HippyNSLogTrace(@"set lock inner !!"); + } + } + + // Do lock inner action! + if (isInner && !self.shouldUnlockInnerScrollView) { + HippyNSLogTrace(@"lock inner (%p) !!!!", sv); + innerScrollView.contentOffset = innerScrollView.lastContentOffset; + } + + // Handle the scenario where the Inner can slide when the Outer's bounces on. + if (HIPPY_NESTED_OPEN_BOUNCES && + self.shouldUnlockInnerScrollView && + isOuter && sv.bounces == YES && + self.dragType == HippyNestedScrollDragTypeBoth && + hasScrollToTheDirectionEdge(outerScrollView, direction)) { + // When the finger is dragging, the Outer has slipped to the edge and is ready to bounce, + // but the Inner can still slide. + // At this time, the sliding of the Outer needs to be locked. + outerScrollView.contentOffset = outerScrollView.lastContentOffset; + HippyNSLogTrace(@"lock outer due to inner scroll"); + } + + // Deal with the multi-level nesting (greater than or equal to three layers). + // If inner has an activeInnerScrollView, that means it has a 'scrollable' nested inside it. + // In this case, if the outer-layer locks inner, it should be passed to the outer of the inner-layer. + if (!self.shouldUnlockInnerScrollView && + isOuter && innerScrollView.activeInnerScrollView) { + innerScrollView.cascadeLockForNestedScroll = YES; + innerScrollView.activeInnerScrollView.cascadeLockForNestedScroll = YES; + if (outerScrollView.cascadeLockForNestedScroll) { + outerScrollView.cascadeLockForNestedScroll = NO; + } + HippyNSLogTrace(@"set cascadeLock to %p", innerScrollView); + } + + // Also need to handle unlock conflicts when multiple levels are nested + // (greater than or equal to three levels) and priorities are different. + // When the inner of the inner-layer and the outer of outer-layer are unlocked at the same time, + // if the inner layer has locked the outer, the outer of outer layer should be locked too. + if (self.shouldUnlockInnerScrollView && + isInner && outerScrollView.activeOuterScrollView) { + outerScrollView.activeOuterScrollView.cascadeLockForNestedScroll = YES; + } + + // Do cascade lock action! + if (isOuter && outerScrollView.cascadeLockForNestedScroll) { + outerScrollView.contentOffset = outerScrollView.lastContentOffset; + HippyNSLogTrace(@"lock outer due to cascadeLock"); + outerScrollView.cascadeLockForNestedScroll = NO; + } else if (isInner && innerScrollView.cascadeLockForNestedScroll) { + innerScrollView.contentOffset = innerScrollView.lastContentOffset; + HippyNSLogTrace(@"lock outer due to cascadeLock"); + innerScrollView.cascadeLockForNestedScroll = NO; + } + } + + // 3. Lock outer scrollview if necessary + else if ([self isDirection:direction hasPriority:HippyNestedScrollPrioritySelf]) { + if (isInner || (isOuter && !self.shouldUnlockOuterScrollView)) { + if (hasScrollToTheDirectionEdge(innerScrollView, direction)) { + self.shouldUnlockOuterScrollView = YES; + HippyNSLogTrace(@"set unlock outer ~"); + } else { + self.shouldUnlockOuterScrollView = NO; + HippyNSLogTrace(@"set lock outer !"); + } + } + + // Handle the effect of outerScroll auto bouncing back when bounces is on. + if (HIPPY_NESTED_OPEN_BOUNCES && + !self.shouldUnlockOuterScrollView && + isOuter && sv.bounces == YES && + self.dragType == HippyNestedScrollDragTypeUndefined && + isScrollInSpringbackState(outerScrollView, direction)) { + self.shouldUnlockOuterScrollView = YES; + } + + // Do lock outer action! + if (self.dragType != HippyNestedScrollDragTypeOuterOnly && + isOuter && !self.shouldUnlockOuterScrollView) { + HippyNSLogTrace(@"lock outer (%p) !!!! %@", sv, NSStringFromCGPoint(outerScrollView.contentOffset)); + outerScrollView.contentOffset = outerScrollView.lastContentOffset; + } + + // Deal with the multi-level nesting (greater than or equal to three layers). + // If the outer has an activeOuterScrollView, this means it has a scrollable nested around it. + // At this point, if the inner-layer lock `Outer`, it should be passed to the Inner in outer-layer. + if (isInner && !self.shouldUnlockOuterScrollView && + outerScrollView.activeOuterScrollView) { + outerScrollView.cascadeLockForNestedScroll = YES; + outerScrollView.activeOuterScrollView.cascadeLockForNestedScroll = YES; + HippyNSLogTrace(@"set cascadeLock to %p", innerScrollView); + } + + // Do cascade lock action! + if (isInner && innerScrollView.cascadeLockForNestedScroll) { + innerScrollView.contentOffset = innerScrollView.lastContentOffset; + HippyNSLogTrace(@"lock outer due to cascadeLock"); + innerScrollView.cascadeLockForNestedScroll = NO; + } else if (isOuter && outerScrollView.cascadeLockForNestedScroll) { + outerScrollView.contentOffset = outerScrollView.lastContentOffset; + HippyNSLogTrace(@"lock outer due to cascadeLock"); + outerScrollView.cascadeLockForNestedScroll = NO; + } + } + + // 4. Update the lastContentOffset record + sv.lastContentOffset = sv.contentOffset; + HippyNSLogTrace(@"end handle %@(%p) scroll -------------", + isOuter ? @"Outer" : @"Inner", sv); +} + + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + if (scrollView == self.outerScrollView) { + self.shouldUnlockOuterScrollView = NO; + HippyNSLogTrace(@"reset outer scroll lock"); + } else if (scrollView == self.innerScrollView) { + self.shouldUnlockInnerScrollView = NO; + HippyNSLogTrace(@"reset inner scroll lock"); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + if (scrollView == self.innerScrollView) { + // record active scroll for filtering events in scrollViewDidScroll + self.outerScrollView.activeInnerScrollView = self.innerScrollView; + self.innerScrollView.activeOuterScrollView = self.outerScrollView; + + self.dragType = HippyNestedScrollDragTypeBoth; + } else if (self.dragType == HippyNestedScrollDragTypeUndefined) { + self.dragType = HippyNestedScrollDragTypeOuterOnly; + } + }); +} + +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { + self.dragType = HippyNestedScrollDragTypeUndefined; +} + + +#pragma mark - HippyNestedScrollGestureDelegate + +- (BOOL)shouldRecognizeScrollGestureSimultaneouslyWithView:(UIView *)view { + // Setup outer scrollview if needed + if (!self.outerScrollView) { + id scrollableView = [self.class findNestedOuterScrollView:self.innerScrollView]; + if (scrollableView) { + [scrollableView addScrollListener:self]; + self.outerScrollView = (UIScrollView *)scrollableView.realScrollView; + } + } + + if (view == self.outerScrollView) { + if (self.nestedScrollPriority > HippyNestedScrollPriorityNone || + self.nestedScrollTopPriority > HippyNestedScrollPriorityNone || + self.nestedScrollBottomPriority > HippyNestedScrollPriorityNone || + self.nestedScrollLeftPriority > HippyNestedScrollPriorityNone || + self.nestedScrollRightPriority > HippyNestedScrollPriorityNone) { + return YES; + } + } else if (self.outerScrollView.nestedGestureDelegate) { + return [self.outerScrollView.nestedGestureDelegate shouldRecognizeScrollGestureSimultaneouslyWithView:view]; + } + return NO; +} + +#pragma mark - Utils + ++ (id)findNestedOuterScrollView:(UIScrollView *)innerScrollView { + // Use superview.superview since scrollview is a subview of hippy view. + UIView *outerScrollView = innerScrollView.superview.superview; + while (outerScrollView) { + if ([outerScrollView conformsToProtocol:@protocol(HippyScrollableProtocol)]) { + break; + } else { + outerScrollView = outerScrollView.superview; + } + } + return (id)outerScrollView; +} + +@end + diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h index 1a4976983bd..a1e8d549ecf 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h @@ -20,18 +20,55 @@ * limitations under the License. */ -#import +#import #import "HippyScrollableProtocol.h" #import "HippyView.h" -@protocol UIScrollViewDelegate; -@interface HippyCustomScrollView : UIScrollView +/// Delegate for handling nested scrolls' gesture conflict +@protocol HippyNestedScrollGestureDelegate +/// Ask the delegate whether gesture should recognize simultaneously +/// For nested scroll +/// @param view the other view +- (BOOL)shouldRecognizeScrollGestureSimultaneouslyWithView:(UIView *)view; + +@end + + +/// Protocol for nested scrollview +@protocol HippyNestedScrollProtocol + +/// Record the last content offset for scroll lock. +@property (nonatomic, assign) CGPoint lastContentOffset; + +/// Record the current active inner scrollable view. +/// Used to judge the responder when outer has more than one inner scrollview. +@property (nonatomic, weak) UIScrollView *activeInnerScrollView; + +/// Record the current active outer scrollable view. +/// Used to pass the cascadeLock when more than three scrollable views nested. +@property (nonatomic, weak) UIScrollView *activeOuterScrollView; + +/// Gesture delegate for handling nested scroll. +@property (nonatomic, weak) id nestedGestureDelegate; + +/// Cascade lock for nestedScroll when +@property (nonatomic, assign) BOOL cascadeLockForNestedScroll; + +@end + + +/// The hippy's custom scrollView +@interface HippyCustomScrollView : UIScrollView + +/// Whether the content needs to be centered. @property (nonatomic, assign) BOOL centerContent; @end + +/// The HippyScrollView component @interface HippyScrollView : HippyView /** diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm index 26a2284dde4..352b9b301a1 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm @@ -26,9 +26,18 @@ #import "UIView+MountEvent.h" #import "UIView+DirectionalLayout.h" #import "HippyRenderUtils.h" +#import "HippyNestedScrollCoordinator.h" + @implementation HippyCustomScrollView +@synthesize lastContentOffset; +@synthesize activeInnerScrollView; +@synthesize activeOuterScrollView; +@synthesize nestedGestureDelegate; +@synthesize cascadeLockForNestedScroll; + + - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { [self.panGestureRecognizer addTarget:self action:@selector(handleCustomPan:)]; @@ -171,6 +180,16 @@ - (void)setContentOffset:(CGPoint)contentOffset { super.contentOffset = contentOffset; } +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer +shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + if (self.nestedGestureDelegate && + gestureRecognizer == self.panGestureRecognizer && + [self.nestedGestureDelegate respondsToSelector:@selector(shouldRecognizeScrollGestureSimultaneouslyWithView:)]) { + return [self.nestedGestureDelegate shouldRecognizeScrollGestureSimultaneouslyWithView:otherGestureRecognizer.view]; + } + return NO; +} + @end @interface HippyScrollView () { @@ -191,6 +210,9 @@ @interface HippyScrollView () { int _recordedScrollIndicatorSwitchValue[2]; // default -1 } +/// Nested scroll coordinator +@property (nonatomic, strong) HippyNestedScrollCoordinator *nestedScrollCoordinator; + @end @implementation HippyScrollView @@ -373,6 +395,44 @@ - (void)zoomToRect:(CGRect)rect animated:(BOOL)animated { [_scrollView zoomToRect:rect animated:animated]; } + +#pragma mark - Nested Scroll + +- (void)setNestedScrollPriority:(HippyNestedScrollPriority)nestedScrollPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollPriority:nestedScrollPriority]; +} + +- (void)setNestedScrollTopPriority:(HippyNestedScrollPriority)nestedScrollTopPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollTopPriority:nestedScrollTopPriority]; +} + +- (void)setNestedScrollLeftPriority:(HippyNestedScrollPriority)nestedScrollLeftPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollLeftPriority:nestedScrollLeftPriority]; +} + +- (void)setNestedScrollBottomPriority:(HippyNestedScrollPriority)nestedScrollBottomPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollBottomPriority:nestedScrollBottomPriority]; +} + +- (void)setNestedScrollRightPriority:(HippyNestedScrollPriority)nestedScrollRightPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollRightPriority:nestedScrollRightPriority]; +} + +- (void)setupNestedScrollCoordinatorIfNeeded { + if (!_nestedScrollCoordinator) { + _nestedScrollCoordinator = [HippyNestedScrollCoordinator new]; + _nestedScrollCoordinator.innerScrollView = _scrollView; + _scrollView.nestedGestureDelegate = _nestedScrollCoordinator; + [self addScrollListener:_nestedScrollCoordinator]; + } +} + + #pragma mark - ScrollView delegate - (void)addScrollListener:(NSObject *)scrollListener { diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollViewManager.mm b/renderer/native/ios/renderer/component/scrollview/HippyScrollViewManager.mm index 7e64562ff82..355ba2a2db0 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollViewManager.mm +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollViewManager.mm @@ -87,6 +87,13 @@ - (UIView *)view { HIPPY_EXPORT_VIEW_PROPERTY(onMomentumScrollEnd, HippyDirectEventBlock) HIPPY_EXPORT_VIEW_PROPERTY(onScrollAnimationEnd, HippyDirectEventBlock) +// Nested scroll props +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollTopPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollLeftPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollBottomPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollRightPriority, HippyNestedScrollPriority) + HIPPY_EXPORT_METHOD(getContentSize:(nonnull NSNumber *)hippyTag callback:(nonnull HippyPromiseResolveBlock)callback) { diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h b/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h index d05c1d04602..601b17d0df7 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h @@ -21,6 +21,7 @@ */ #import +#import "HippyConvert+NativeRender.h" /** * Protocol for any scrollable components inherit from UIScrollView @@ -76,4 +77,23 @@ */ - (void)scrollToIndex:(NSInteger)index animated:(BOOL)animated; + +#pragma mark - Nested Scroll Props + +/// Priority of nestedScroll, see `HippyNestedScrollCoordinator` for more +- (void)setNestedScrollPriority:(HippyNestedScrollPriority)nestedScrollPriority; + +/// Priority of nestedScroll in specific direction (finger move from bottom to top) +- (void)setNestedScrollTopPriority:(HippyNestedScrollPriority)nestedScrollTopPriority; + +/// Priority of nestedScroll in specific direction (finger move from right to left) +- (void)setNestedScrollLeftPriority:(HippyNestedScrollPriority)nestedScrollLeftPriority; + +/// Priority of nestedScroll in specific direction (finger move from top to bottom) +- (void)setNestedScrollBottomPriority:(HippyNestedScrollPriority)nestedScrollBottomPriority; + +/// Set priority of nestedScroll in specific direction (finger move from left to right) +- (void)setNestedScrollRightPriority:(HippyNestedScrollPriority)nestedScrollRightPriority; + + @end diff --git a/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm b/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm index e95500f7efc..2220e305e57 100644 --- a/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm +++ b/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm @@ -33,6 +33,7 @@ #import "HippyWaterfallViewCell.h" #import "HippyRootView.h" #import "HippyShadowListView.h" +#import "HippyNestedScrollCoordinator.h" static NSString *kCellIdentifier = @"HippyWaterfallCellIdentifier"; @@ -56,6 +57,9 @@ @interface HippyWaterfallView () { /// Hippy root view @property (nonatomic, weak) HippyRootView *rootView; +/// Nested scroll coordinator +@property (nonatomic, strong) HippyNestedScrollCoordinator *nestedScrollCoordinator; + @end @implementation HippyWaterfallView { @@ -71,15 +75,49 @@ - (instancetype)initWithFrame:(CGRect)frame { _scrollListeners = [NSHashTable weakObjectsHashTable]; _scrollEventThrottle = 100.f; _cachedWeakCellViews = [NSMapTable strongToWeakObjectsMapTable]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveMemoryWarning) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; [self initCollectionView]; - if (@available(iOS 11.0, *)) { - self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; - } } return self; } +- (void)setupNestedScrollCoordinatorIfNeeded { + if (!_nestedScrollCoordinator) { + _nestedScrollCoordinator = [HippyNestedScrollCoordinator new]; + _nestedScrollCoordinator.innerScrollView = self.collectionView; + self.collectionView.nestedGestureDelegate = _nestedScrollCoordinator; + [self addScrollListener:_nestedScrollCoordinator]; + } +} + +- (void)setNestedScrollPriority:(HippyNestedScrollPriority)nestedScrollPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollPriority:nestedScrollPriority]; +} + +- (void)setNestedScrollTopPriority:(HippyNestedScrollPriority)nestedScrollTopPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollTopPriority:nestedScrollTopPriority]; +} + +- (void)setNestedScrollLeftPriority:(HippyNestedScrollPriority)nestedScrollLeftPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollLeftPriority:nestedScrollLeftPriority]; +} + +- (void)setNestedScrollBottomPriority:(HippyNestedScrollPriority)nestedScrollBottomPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollBottomPriority:nestedScrollBottomPriority]; +} + +- (void)setNestedScrollRightPriority:(HippyNestedScrollPriority)nestedScrollRightPriority { + [self setupNestedScrollCoordinatorIfNeeded]; + [self.nestedScrollCoordinator setNestedScrollRightPriority:nestedScrollRightPriority]; +} + - (void)initCollectionView { _layout = [self collectionViewLayout]; HippyNextListTableView *collectionView = [[HippyNextListTableView alloc] initWithFrame:self.bounds collectionViewLayout:_layout]; @@ -89,6 +127,7 @@ - (void)initCollectionView { collectionView.layoutDelegate = self; collectionView.alwaysBounceVertical = YES; collectionView.backgroundColor = [UIColor clearColor]; + collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; _collectionView = collectionView; [self registerCells]; [self registerSupplementaryViews]; diff --git a/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallViewManager.m b/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallViewManager.m index c1ac185eef0..a24f8f4b660 100644 --- a/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallViewManager.m +++ b/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallViewManager.m @@ -46,6 +46,11 @@ @implementation HippyWaterfallViewManager HIPPY_EXPORT_VIEW_PROPERTY(containPullFooter, BOOL) HIPPY_EXPORT_VIEW_PROPERTY(scrollEventThrottle, double) HIPPY_EXPORT_VIEW_PROPERTY(onScroll, HippyDirectEventBlock) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollTopPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollLeftPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollBottomPriority, HippyNestedScrollPriority) +HIPPY_EXPORT_VIEW_PROPERTY(nestedScrollRightPriority, HippyNestedScrollPriority) HIPPY_REMAP_VIEW_PROPERTY(showScrollIndicator, collectionView.showsVerticalScrollIndicator, BOOL) - (UIView *)view { diff --git a/renderer/native/ios/utils/HippyConvert+NativeRender.h b/renderer/native/ios/utils/HippyConvert+NativeRender.h index 54b3b9cd084..dc5d2fda840 100644 --- a/renderer/native/ios/utils/HippyConvert+NativeRender.h +++ b/renderer/native/ios/utils/HippyConvert+NativeRender.h @@ -114,4 +114,20 @@ typedef NS_ENUM(NSInteger, HippyPaintType) { @end + +typedef NS_ENUM(char, HippyNestedScrollPriority) { + HippyNestedScrollPriorityUndefined = 0, + HippyNestedScrollPriorityNone, + HippyNestedScrollPrioritySelf, + HippyNestedScrollPriorityParent, +}; + +@interface HippyConvert (NestedScroll) + +/// Convert NestedScroll config to enum +/// - Parameter json: string ++ (HippyNestedScrollPriority)HippyNestedScrollPriority:(id)json; + +@end + NS_ASSUME_NONNULL_END diff --git a/renderer/native/ios/utils/HippyConvert+NativeRender.m b/renderer/native/ios/utils/HippyConvert+NativeRender.m index 80804de7fac..7617f447c70 100644 --- a/renderer/native/ios/utils/HippyConvert+NativeRender.m +++ b/renderer/native/ios/utils/HippyConvert+NativeRender.m @@ -231,3 +231,13 @@ @implementation HippyConvert (HippyPaintType) }), HippyPaintTypeUndefined, integerValue) @end + +@implementation HippyConvert (NestedScroll) + +HIPPY_ENUM_CONVERTER(HippyNestedScrollPriority, (@{ + @"none": @(HippyNestedScrollPriorityNone), + @"self": @(HippyNestedScrollPrioritySelf), + @"parent": @(HippyNestedScrollPriorityParent), +}), HippyNestedScrollPriorityUndefined, charValue) + +@end From 9c3d9ead42b4a2b535e641d0ae7751ad563acf23 Mon Sep 17 00:00:00 2001 From: wwwcg Date: Tue, 10 Dec 2024 22:01:11 +0800 Subject: [PATCH 2/5] feat(ios): nested scroll api support part2 --- .../listview/HippyNextListTableView.m | 1 + .../scrollview/HippyNestedScrollCoordinator.m | 23 ++++++++++++------- .../component/scrollview/HippyScrollView.h | 6 ++++- .../component/scrollview/HippyScrollView.mm | 20 ++++++++++++---- .../waterfalllist/HippyWaterfallView.mm | 18 +++++++++++---- 5 files changed, 49 insertions(+), 19 deletions(-) diff --git a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m index b97e8384a64..d22b2d54a3b 100644 --- a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m +++ b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m @@ -30,6 +30,7 @@ @implementation HippyNextListTableView @synthesize activeOuterScrollView; @synthesize nestedGestureDelegate; @synthesize cascadeLockForNestedScroll; +@synthesize isLockedInNestedScroll; /** * we need scroll indicator to be at top diff --git a/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m index 121b1240325..4d65511beb4 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m +++ b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m @@ -76,6 +76,9 @@ - (void)setOuterScrollView:(UIScrollView *)outerScrol _outerScrollView.bounces = NO; } + +#pragma mark - Private + - (BOOL)isDirection:(HippyNestedScrollDirection)direction hasPriority:(HippyNestedScrollPriority)priority { // Note that the top and bottom defined in the nestedScroll attribute refer to the finger orientation, // as opposed to the page orientation. @@ -138,6 +141,10 @@ static inline BOOL isScrollInSpringbackState(const UIScrollView *scrollview, return NO; } +static inline void lockScrollView(const UIScrollView *scrollView) { + scrollView.contentOffset = scrollView.lastContentOffset; + scrollView.isLockedInNestedScroll = YES; +} #pragma mark - ScrollEvents Delegate @@ -199,7 +206,7 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // Do lock inner action! if (isInner && !self.shouldUnlockInnerScrollView) { HippyNSLogTrace(@"lock inner (%p) !!!!", sv); - innerScrollView.contentOffset = innerScrollView.lastContentOffset; + lockScrollView(innerScrollView); } // Handle the scenario where the Inner can slide when the Outer's bounces on. @@ -211,7 +218,7 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // When the finger is dragging, the Outer has slipped to the edge and is ready to bounce, // but the Inner can still slide. // At this time, the sliding of the Outer needs to be locked. - outerScrollView.contentOffset = outerScrollView.lastContentOffset; + lockScrollView(outerScrollView); HippyNSLogTrace(@"lock outer due to inner scroll"); } @@ -239,11 +246,11 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // Do cascade lock action! if (isOuter && outerScrollView.cascadeLockForNestedScroll) { - outerScrollView.contentOffset = outerScrollView.lastContentOffset; + lockScrollView(outerScrollView); HippyNSLogTrace(@"lock outer due to cascadeLock"); outerScrollView.cascadeLockForNestedScroll = NO; } else if (isInner && innerScrollView.cascadeLockForNestedScroll) { - innerScrollView.contentOffset = innerScrollView.lastContentOffset; + lockScrollView(innerScrollView); HippyNSLogTrace(@"lock outer due to cascadeLock"); innerScrollView.cascadeLockForNestedScroll = NO; } @@ -273,8 +280,8 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // Do lock outer action! if (self.dragType != HippyNestedScrollDragTypeOuterOnly && isOuter && !self.shouldUnlockOuterScrollView) { - HippyNSLogTrace(@"lock outer (%p) !!!! %@", sv, NSStringFromCGPoint(outerScrollView.contentOffset)); - outerScrollView.contentOffset = outerScrollView.lastContentOffset; + HippyNSLogTrace(@"lock outer (%p) !!!!", sv); + lockScrollView(outerScrollView); } // Deal with the multi-level nesting (greater than or equal to three layers). @@ -289,11 +296,11 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // Do cascade lock action! if (isInner && innerScrollView.cascadeLockForNestedScroll) { - innerScrollView.contentOffset = innerScrollView.lastContentOffset; + lockScrollView(innerScrollView); HippyNSLogTrace(@"lock outer due to cascadeLock"); innerScrollView.cascadeLockForNestedScroll = NO; } else if (isOuter && outerScrollView.cascadeLockForNestedScroll) { - outerScrollView.contentOffset = outerScrollView.lastContentOffset; + lockScrollView(outerScrollView); HippyNSLogTrace(@"lock outer due to cascadeLock"); outerScrollView.cascadeLockForNestedScroll = NO; } diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h index a1e8d549ecf..50c9c5c109d 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h @@ -53,9 +53,13 @@ /// Gesture delegate for handling nested scroll. @property (nonatomic, weak) id nestedGestureDelegate; -/// Cascade lock for nestedScroll when +/// Cascade lock for nestedScroll @property (nonatomic, assign) BOOL cascadeLockForNestedScroll; +/// Whether is temporarily locked in current DidScroll callback. +/// It is used to determine whether to block the sending of onScroll events. +@property (nonatomic, assign) BOOL isLockedInNestedScroll; + @end diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm index 352b9b301a1..d3c6955df62 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm @@ -36,6 +36,7 @@ @implementation HippyCustomScrollView @synthesize activeOuterScrollView; @synthesize nestedGestureDelegate; @synthesize cascadeLockForNestedScroll; +@synthesize isLockedInNestedScroll; - (instancetype)initWithFrame:(CGRect)frame { @@ -469,6 +470,20 @@ - (NSDictionary *)scrollEventBody { } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { + for (NSObject *scrollViewListener in _scrollListeners) { + if ([scrollViewListener respondsToSelector:@selector(scrollViewDidScroll:)]) { + [scrollViewListener scrollViewDidScroll:scrollView]; + } + } + + id sv = (id)scrollView; + if (sv.isLockedInNestedScroll) { + // This method is still called when nested scrolling, + // and we should ignore subsequent logic execution when simulating locking. + sv.isLockedInNestedScroll = NO; // reset + return; + } + NSTimeInterval now = CACurrentMediaTime(); NSTimeInterval ti = now - _lastScrollDispatchTime; BOOL flag = (_scrollEventThrottle > 0 && _scrollEventThrottle < ti); @@ -479,11 +494,6 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { _lastScrollDispatchTime = now; _allowNextScrollNoMatterWhat = NO; } - for (NSObject *scrollViewListener in _scrollListeners) { - if ([scrollViewListener respondsToSelector:@selector(scrollViewDidScroll:)]) { - [scrollViewListener scrollViewDidScroll:scrollView]; - } - } } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { diff --git a/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm b/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm index 2220e305e57..93330d7d8a0 100644 --- a/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm +++ b/renderer/native/ios/renderer/component/waterfalllist/HippyWaterfallView.mm @@ -441,6 +441,19 @@ - (BOOL)manualScroll { #pragma mark - UIScrollView Delegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { + for (NSObject *scrollViewListener in [self scrollListeners]) { + if ([scrollViewListener respondsToSelector:@selector(scrollViewDidScroll:)]) { + [scrollViewListener scrollViewDidScroll:scrollView]; + } + } + id sv = (id)scrollView; + if (sv.isLockedInNestedScroll) { + // This method is still called when nested scrolling, + // and we should ignore subsequent logic execution when simulating locking. + sv.isLockedInNestedScroll = NO; // reset + return; + } + if (_onScroll) { CFTimeInterval now = CACurrentMediaTime(); CFTimeInterval ti = (now - _lastOnScrollEventTimeInterval) * 1000.0; @@ -451,11 +464,6 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { _allowNextScrollNoMatterWhat = NO; } } - for (NSObject *scrollViewListener in [self scrollListeners]) { - if ([scrollViewListener respondsToSelector:@selector(scrollViewDidScroll:)]) { - [scrollViewListener scrollViewDidScroll:scrollView]; - } - } [_headerRefreshView scrollViewDidScroll:scrollView]; [_footerRefreshView scrollViewDidScroll:scrollView]; } From 34be41ddb074fd8d651d703ccc0493500d484fa9 Mon Sep 17 00:00:00 2001 From: wwwcg Date: Wed, 11 Dec 2024 14:38:37 +0800 Subject: [PATCH 3/5] feat(ios): nested scroll api support part3 --- .../scrollview/HippyNestedScrollCoordinator.m | 14 ++++++++++---- .../component/scrollview/HippyScrollableProtocol.h | 3 +++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m index 4d65511beb4..f6ed3c4b8f5 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m +++ b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollCoordinator.m @@ -370,13 +370,19 @@ - (BOOL)shouldRecognizeScrollGestureSimultaneouslyWithView:(UIView *)view { + (id)findNestedOuterScrollView:(UIScrollView *)innerScrollView { // Use superview.superview since scrollview is a subview of hippy view. - UIView *outerScrollView = innerScrollView.superview.superview; + UIView *innerScrollable = (UIView *)innerScrollView.superview; + UIView *outerScrollView = innerScrollable.superview; while (outerScrollView) { if ([outerScrollView conformsToProtocol:@protocol(HippyScrollableProtocol)]) { - break; - } else { - outerScrollView = outerScrollView.superview; + UIView *outerScrollable = (UIView *)outerScrollView; + // Make sure to find scrollable with same direction. + BOOL isInnerHorizontal = [innerScrollable respondsToSelector:@selector(horizontal)] ? [innerScrollable horizontal] : NO; + BOOL isOuterHorizontal = [outerScrollable respondsToSelector:@selector(horizontal)] ? [outerScrollable horizontal] : NO; + if (isInnerHorizontal == isOuterHorizontal) { + break; + } } + outerScrollView = outerScrollView.superview; } return (id)outerScrollView; } diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h b/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h index 601b17d0df7..b0247faa981 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollableProtocol.h @@ -61,6 +61,9 @@ @optional +/// Return whether is horizontal, optional, default NO. +- (BOOL)horizontal; + /** * Set components scroll to location offset * From c04741b37fbf6a676f7ca9e5164f5303de7ff61b Mon Sep 17 00:00:00 2001 From: wwwcg Date: Wed, 11 Dec 2024 16:34:43 +0800 Subject: [PATCH 4/5] feat(ios): nested scroll api support, add test --- .../listview/HippyNextListTableView.h | 2 +- .../listview/HippyNextListTableView.m | 8 +- .../scrollview/HippyNestedScrollProtocol.h | 75 ++++++++++++++ .../component/scrollview/HippyScrollView.h | 41 +------- .../component/scrollview/HippyScrollView.mm | 8 +- tests/ios/HippyNestedScrollTest.m | 98 +++++++++++++++++++ 6 files changed, 178 insertions(+), 54 deletions(-) create mode 100644 renderer/native/ios/renderer/component/scrollview/HippyNestedScrollProtocol.h create mode 100644 tests/ios/HippyNestedScrollTest.m diff --git a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h index 44afe84f6cb..1041e062060 100644 --- a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h +++ b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.h @@ -21,7 +21,7 @@ */ #import -#import "HippyScrollView.h" +#import "HippyNestedScrollProtocol.h" NS_ASSUME_NONNULL_BEGIN diff --git a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m index d22b2d54a3b..3e848e0c6b6 100644 --- a/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m +++ b/renderer/native/ios/renderer/component/listview/HippyNextListTableView.m @@ -21,16 +21,10 @@ */ #import "HippyNextListTableView.h" -#import "HippyScrollView.h" @implementation HippyNextListTableView -@synthesize lastContentOffset; -@synthesize activeInnerScrollView; -@synthesize activeOuterScrollView; -@synthesize nestedGestureDelegate; -@synthesize cascadeLockForNestedScroll; -@synthesize isLockedInNestedScroll; +HIPPY_NESTEDSCROLL_PROTOCOL_PROPERTY_IMP /** * we need scroll indicator to be at top diff --git a/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollProtocol.h b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollProtocol.h new file mode 100644 index 00000000000..167b4be6357 --- /dev/null +++ b/renderer/native/ios/renderer/component/scrollview/HippyNestedScrollProtocol.h @@ -0,0 +1,75 @@ +/*! + * iOS SDK + * + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef HippyNestedScrollProtocol_h +#define HippyNestedScrollProtocol_h + +#import + + +#define HIPPY_NESTEDSCROLL_PROTOCOL_PROPERTY_IMP \ +@synthesize lastContentOffset; \ +@synthesize activeInnerScrollView; \ +@synthesize activeOuterScrollView; \ +@synthesize nestedGestureDelegate; \ +@synthesize cascadeLockForNestedScroll; \ +@synthesize isLockedInNestedScroll; \ + + +/// Delegate for handling nested scrolls' gesture conflict +@protocol HippyNestedScrollGestureDelegate + +/// Ask the delegate whether gesture should recognize simultaneously +/// For nested scroll +/// @param view the other view +- (BOOL)shouldRecognizeScrollGestureSimultaneouslyWithView:(UIView *)view; + +@end + + +/// Protocol for nested scrollview +@protocol HippyNestedScrollProtocol + +/// Record the last content offset for scroll lock. +@property (nonatomic, assign) CGPoint lastContentOffset; + +/// Record the current active inner scrollable view. +/// Used to judge the responder when outer has more than one inner scrollview. +@property (nonatomic, weak) UIScrollView *activeInnerScrollView; + +/// Record the current active outer scrollable view. +/// Used to pass the cascadeLock when more than three scrollable views nested. +@property (nonatomic, weak) UIScrollView *activeOuterScrollView; + +/// Gesture delegate for handling nested scroll. +@property (nonatomic, weak) id nestedGestureDelegate; + +/// Cascade lock for nestedScroll +@property (nonatomic, assign) BOOL cascadeLockForNestedScroll; + +/// Whether is temporarily locked in current DidScroll callback. +/// It is used to determine whether to block the sending of onScroll events. +@property (nonatomic, assign) BOOL isLockedInNestedScroll; + +@end + +#endif /* HippyNestedScrollProtocol_h */ diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h index 50c9c5c109d..3d10e5e47b3 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.h @@ -21,46 +21,9 @@ */ #import -#import "HippyScrollableProtocol.h" #import "HippyView.h" - - -/// Delegate for handling nested scrolls' gesture conflict -@protocol HippyNestedScrollGestureDelegate - -/// Ask the delegate whether gesture should recognize simultaneously -/// For nested scroll -/// @param view the other view -- (BOOL)shouldRecognizeScrollGestureSimultaneouslyWithView:(UIView *)view; - -@end - - -/// Protocol for nested scrollview -@protocol HippyNestedScrollProtocol - -/// Record the last content offset for scroll lock. -@property (nonatomic, assign) CGPoint lastContentOffset; - -/// Record the current active inner scrollable view. -/// Used to judge the responder when outer has more than one inner scrollview. -@property (nonatomic, weak) UIScrollView *activeInnerScrollView; - -/// Record the current active outer scrollable view. -/// Used to pass the cascadeLock when more than three scrollable views nested. -@property (nonatomic, weak) UIScrollView *activeOuterScrollView; - -/// Gesture delegate for handling nested scroll. -@property (nonatomic, weak) id nestedGestureDelegate; - -/// Cascade lock for nestedScroll -@property (nonatomic, assign) BOOL cascadeLockForNestedScroll; - -/// Whether is temporarily locked in current DidScroll callback. -/// It is used to determine whether to block the sending of onScroll events. -@property (nonatomic, assign) BOOL isLockedInNestedScroll; - -@end +#import "HippyScrollableProtocol.h" +#import "HippyNestedScrollProtocol.h" /// The hippy's custom scrollView diff --git a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm index d3c6955df62..dd466052bed 100644 --- a/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm +++ b/renderer/native/ios/renderer/component/scrollview/HippyScrollView.mm @@ -31,13 +31,7 @@ @implementation HippyCustomScrollView -@synthesize lastContentOffset; -@synthesize activeInnerScrollView; -@synthesize activeOuterScrollView; -@synthesize nestedGestureDelegate; -@synthesize cascadeLockForNestedScroll; -@synthesize isLockedInNestedScroll; - +HIPPY_NESTEDSCROLL_PROTOCOL_PROPERTY_IMP - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { diff --git a/tests/ios/HippyNestedScrollTest.m b/tests/ios/HippyNestedScrollTest.m new file mode 100644 index 00000000000..1f51bad2268 --- /dev/null +++ b/tests/ios/HippyNestedScrollTest.m @@ -0,0 +1,98 @@ +/*! + * iOS SDK + * + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import + + +@interface HippyNestedScrollCoordinator (UnitTest) + +/// Whether is the given direction has specified priority +/// direction param see `HippyNestedScrollDirection` +- (BOOL)isDirection:(char)direction hasPriority:(HippyNestedScrollPriority)priority; + +@end + +@interface HippyNestedScrollTest : XCTestCase + +@end + +@implementation HippyNestedScrollTest + +- (void)setUp { + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. +} + +- (void)testNestedScrollCoordinatorSetPriority { + HippyNestedScrollCoordinator *coordinator = [HippyNestedScrollCoordinator new]; + XCTAssertTrue([coordinator isDirection:0 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:1 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:2 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:3 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:4 hasPriority:HippyNestedScrollPrioritySelf]); + + coordinator.nestedScrollPriority = HippyNestedScrollPrioritySelf; + XCTAssertTrue([coordinator isDirection:1 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:2 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:3 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:4 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertFalse([coordinator isDirection:1 hasPriority:HippyNestedScrollPriorityParent]); + XCTAssertFalse([coordinator isDirection:2 hasPriority:HippyNestedScrollPriorityParent]); + XCTAssertFalse([coordinator isDirection:3 hasPriority:HippyNestedScrollPriorityParent]); + XCTAssertFalse([coordinator isDirection:4 hasPriority:HippyNestedScrollPriorityParent]); + + coordinator.nestedScrollRightPriority = HippyNestedScrollPriorityParent; + coordinator.nestedScrollLeftPriority = HippyNestedScrollPrioritySelf; + coordinator.nestedScrollBottomPriority = HippyNestedScrollPriorityNone; + coordinator.nestedScrollTopPriority = HippyNestedScrollPriorityParent; + XCTAssertTrue([coordinator isDirection:1 hasPriority:HippyNestedScrollPriorityParent]); + XCTAssertTrue([coordinator isDirection:2 hasPriority:HippyNestedScrollPrioritySelf]); + XCTAssertTrue([coordinator isDirection:3 hasPriority:HippyNestedScrollPriorityNone]); + XCTAssertTrue([coordinator isDirection:4 hasPriority:HippyNestedScrollPriorityParent]); +} + +- (void)testShouldRecognizeScrollGestureSimultaneously { + HippyNestedScrollCoordinator *coordinator = [HippyNestedScrollCoordinator new]; + HippyScrollView *scrollView = [HippyScrollView new]; + coordinator.outerScrollView = (UIScrollView *)scrollView.realScrollView; + XCTAssertFalse([coordinator shouldRecognizeScrollGestureSimultaneouslyWithView:scrollView.realScrollView]); + coordinator.nestedScrollPriority = HippyNestedScrollPriorityNone; + XCTAssertFalse([coordinator shouldRecognizeScrollGestureSimultaneouslyWithView:scrollView.realScrollView]); + coordinator.nestedScrollPriority = HippyNestedScrollPrioritySelf; + XCTAssertTrue([coordinator shouldRecognizeScrollGestureSimultaneouslyWithView:scrollView.realScrollView]); +} + +- (void)testNestedScrollDoScrollViewDidScroll { + HippyNestedScrollCoordinator *coordinator = [HippyNestedScrollCoordinator new]; + HippyScrollView *scrollView = [HippyScrollView new]; + UIScrollView *sv = (UIScrollView *)scrollView.realScrollView; + [sv setContentOffset:CGPointMake(100.0, 200.0)]; + XCTAssert(CGPointEqualToPoint(sv.lastContentOffset, CGPointZero)); + [coordinator scrollViewDidScroll:scrollView.realScrollView]; + XCTAssert(CGPointEqualToPoint(sv.lastContentOffset, CGPointMake(100.0, 200.0))); +} + +@end From f9700b006de4939cbf9444a3a216f5e56c298f63 Mon Sep 17 00:00:00 2001 From: wwwcg Date: Wed, 11 Dec 2024 17:05:47 +0800 Subject: [PATCH 5/5] docs(js): add nested scroll api's docs --- docs/api/hippy-react/components.md | 36 ++++++++++++++++++++++++++++++ docs/api/hippy-vue/components.md | 36 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/docs/api/hippy-react/components.md b/docs/api/hippy-react/components.md index d625e1e11ed..59e903df54b 100644 --- a/docs/api/hippy-react/components.md +++ b/docs/api/hippy-react/components.md @@ -130,6 +130,24 @@ import icon from './qb_icon_new.png'; | editable | 是否可编辑,开启侧滑删除时需要设置为 `true`。`最低支持版本2.9.0` | `boolean` | `iOS` | | delText | 侧滑删除文本。`最低支持版本2.9.0` | `string` | `iOS` | | onDelete | 在列表项侧滑删除时调起。`最低支持版本2.9.0` | `(nativeEvent: { index: number}) => void` | `iOS` | +| nestedScrollPriority* | 嵌套滚动事件处理优先级,`default:self`。相当于同时设置 `nestedScrollLeftPriority`、 `nestedScrollTopPriority`、 `nestedScrollRightPriority`、 `nestedScrollBottomPriority`。 `Android最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollLeftPriority | 嵌套时**从右往左**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollTopPriority | 嵌套时**从下往上**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollRightPriority | 嵌套时**从左往右**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollBottomPriority | 嵌套时**从上往下**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | + +* nestedScrollPriority 的参数含义: + + * `self`:当前组件优先,滚动事件将先由当前组件消费,剩余部分传递给父组件消费; + + * `parent`:父组件优先,滚动事件将先由父组件消费,剩余部分再由当前组件消费; + + * `none`:不允许嵌套滚动,滚动事件将不会传递给父组件。 + +* nestedScrollPriority 默认值的说明: + + 如未设置任何滚动优先级时,iOS平台的默认值为`none`,即与系统默认行为保持一致。当指定任意一方向的优先级后,其他方向默认值为`self`; + Android平台默认值始终为`self`。 ## 方法 @@ -253,6 +271,24 @@ import icon from './qb_icon_new.png'; | showScrollIndicator | 是否显示滚动条。 `default: false` | `boolean` | `Android、hippy-react-web、Voltron` | | showsHorizontalScrollIndicator | 当此值设为 `false` 的时候,`ScrollView` 会隐藏水平的滚动条。`default: true` | `boolean` | `iOS、Voltron` | | showsVerticalScrollIndicator | 当此值设为 `false` 的时候,`ScrollView` 会隐藏垂直的滚动条。 `default: true` | `boolean` | `iOS、Voltron` | +| nestedScrollPriority* | 嵌套滚动事件处理优先级,`default:self`。相当于同时设置 `nestedScrollLeftPriority`、 `nestedScrollTopPriority`、 `nestedScrollRightPriority`、 `nestedScrollBottomPriority`。 `Android最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollLeftPriority | 嵌套时**从右往左**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollTopPriority | 嵌套时**从下往上**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollRightPriority | 嵌套时**从左往右**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollBottomPriority | 嵌套时**从上往下**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | + +* nestedScrollPriority 的参数含义: + + * `self`(默认值):当前组件优先,滚动事件将先由当前组件消费,剩余部分传递给父组件消费; + + * `parent`:父组件优先,滚动事件将先由父组件消费,剩余部分再由当前组件消费; + + * `none`:不允许嵌套滚动,滚动事件将不会传递给父组件。 + +* nestedScrollPriority 默认值的说明: + + 如未设置任何滚动优先级时,iOS平台的默认值为`none`,即与系统默认行为保持一致。当指定任意一方向的优先级后,其他方向默认值为`self`; + Android平台默认值始终为`self`。 ## 方法 diff --git a/docs/api/hippy-vue/components.md b/docs/api/hippy-vue/components.md index f1ed07144cb..c2fa9c550e1 100644 --- a/docs/api/hippy-vue/components.md +++ b/docs/api/hippy-vue/components.md @@ -68,6 +68,24 @@ | showsVerticalScrollIndicator | 当此值设为 `false` 的时候,`ScrollView` 会隐藏垂直的滚动条。 `default: true` `(仅在 overflow-y/x: scroll 时适用)`| `boolean` | `iOS、Voltron` | | nativeBackgroundAndroid | 配置水波纹效果,`最低支持版本 2.13.1`;配置项为 `{ borderless: boolean, color: Color, rippleRadius: number }`; `borderless` 表示波纹是否有边界,默认false;`color` 波纹颜色;`rippleRadius` 波纹半径,若不设置,默认容器边框为边界; `注意:设置水波纹后默认不显示,需要在对应触摸事件中调用 setPressed 和 setHotspot 方法进行水波纹展示,详情参考相关`[demo](//github.com/Tencent/Hippy/tree/master/examples/hippy-vue-demo/src/components/demos/demo-div.vue) | `Object`| `Android` | | pointerEvents | 用于控制视图是否可以成为触摸事件的目标。 | `enum('box-none', 'none', 'box-only', 'auto')` | `iOS` | +| nestedScrollPriority* | 嵌套滚动事件处理优先级,`default:self`。相当于同时设置 `nestedScrollLeftPriority`、 `nestedScrollTopPriority`、 `nestedScrollRightPriority`、 `nestedScrollBottomPriority`。 `Android最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollLeftPriority | 嵌套时**从右往左**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollTopPriority | 嵌套时**从下往上**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollRightPriority | 嵌套时**从左往右**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollBottomPriority | 嵌套时**从上往下**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | + +* nestedScrollPriority 的参数含义: + + * `self`(默认值):当前组件优先,滚动事件将先由当前组件消费,剩余部分传递给父组件消费; + + * `parent`:父组件优先,滚动事件将先由父组件消费,剩余部分再由当前组件消费; + + * `none`:不允许嵌套滚动,滚动事件将不会传递给父组件。 + +* nestedScrollPriority 默认值的说明: + + 如未设置任何滚动优先级时,iOS平台的默认值为`none`,即与系统默认行为保持一致。当指定任意一方向的优先级后,其他方向默认值为`self`; + Android平台默认值始终为`self`。 * pointerEvents 的参数含义: * `auto`(默认值) - 视图可以是触摸事件的目标; @@ -379,6 +397,24 @@ Hippy 的重点功能,高性能的可复用列表组件,在终端侧会被 | endReached | 当所有的数据都已经渲染过,并且列表被滚动到最后一条时,将触发 `endReached` 回调。 | `Function` | `Android、iOS、Web-Renderer、Voltron` | | editable | 是否可编辑,开启侧滑删除时需要设置为 `true`。`最低支持版本2.9.0` | `boolean` | `iOS` | | delText | 侧滑删除文本。`最低支持版本2.9.0` | `string` | `iOS` | +| nestedScrollPriority* | 嵌套滚动事件处理优先级,`default:self`。相当于同时设置 `nestedScrollLeftPriority`、 `nestedScrollTopPriority`、 `nestedScrollRightPriority`、 `nestedScrollBottomPriority`。 `Android最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollLeftPriority | 嵌套时**从右往左**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollTopPriority | 嵌套时**从下往上**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollRightPriority | 嵌套时**从左往右**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | +| nestedScrollBottomPriority | 嵌套时**从上往下**滚动事件的处理优先级,会覆盖 `nestedScrollPriority` 对应方向的值。`最低支持版本 2.16.0,iOS最低支持版本3.3.3` | `enum(self,parent,none)` | `Android、iOS` | + +* nestedScrollPriority 的参数含义: + + * `self`(默认值):当前组件优先,滚动事件将先由当前组件消费,剩余部分传递给父组件消费; + + * `parent`:父组件优先,滚动事件将先由父组件消费,剩余部分再由当前组件消费; + + * `none`:不允许嵌套滚动,滚动事件将不会传递给父组件。 + +* nestedScrollPriority 默认值的说明: + + 如未设置任何滚动优先级时,iOS平台的默认值为`none`,即与系统默认行为保持一致。当指定任意一方向的优先级后,其他方向默认值为`self`; + Android平台默认值始终为`self`。 ## 事件