From faf24838c83c69c1cbe89633cdf5c425be683375 Mon Sep 17 00:00:00 2001 From: wwwcg Date: Mon, 29 Jul 2024 17:05:14 +0800 Subject: [PATCH 1/6] feat(ios): refreshWrapper support viewPager --- .../refreshview/HippyRefreshWrapper.h | 19 +++ .../refreshview/HippyRefreshWrapper.m | 114 ++++++++++++------ .../refreshview/HippyRefreshWrapperItemView.m | 8 +- .../HippyRefreshWrapperViewManager.m | 6 +- ios/sdk/component/viewPager/HippyViewPager.h | 2 +- ios/sdk/component/viewPager/HippyViewPager.m | 11 +- 6 files changed, 113 insertions(+), 47 deletions(-) diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapper.h b/ios/sdk/component/refreshview/HippyRefreshWrapper.h index 16ee607e5d1..85d745756e0 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapper.h +++ b/ios/sdk/component/refreshview/HippyRefreshWrapper.h @@ -21,12 +21,31 @@ */ #import +#import "HippyComponent.h" #import "HippyInvalidating.h" + NS_ASSUME_NONNULL_BEGIN + @class HippyBridge; + +/// RefreshWrapper add refresh capability to scrollable components such as ListView @interface HippyRefreshWrapper : UIView + +/// Direction of Refresh +@property (nonatomic, assign, getter=isHorizontal) BOOL horizontal; + +/// Bounce time of refresh start/end animation +@property (nonatomic, assign) CGFloat bounceTime; + +/// The onRefresh block that JS side binding. +@property (nonatomic, copy) HippyDirectEventBlock onRefresh; + +/// Call to indicate refresh completion. - (void)refreshCompleted; + +/// Call to start the refresh process. - (void)startRefresh; + @end NS_ASSUME_NONNULL_END diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapper.m b/ios/sdk/component/refreshview/HippyRefreshWrapper.m index 7898cb28d4e..dcbee8378f9 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapper.m +++ b/ios/sdk/component/refreshview/HippyRefreshWrapper.m @@ -24,14 +24,21 @@ #import "UIView+Hippy.h" #import "HippyRefreshWrapperItemView.h" #import "HippyScrollableProtocol.h" + + +static NSTimeInterval const kHippyDefaultRefreshBounceTime = 400.0; + @interface HippyRefreshWrapper () + +/// The child view of RefreshWrapper @property (nonatomic, weak) HippyRefreshWrapperItemView *wrapperItemView; +/// Scrollable target @property (nonatomic, weak) id scrollableView; -@property (nonatomic, copy) HippyDirectEventBlock onRefresh; -@property (nonatomic, assign) CGFloat bounceTime; -@property (nonatomic, weak) HippyBridge *bridge; + @end + @implementation HippyRefreshWrapper + - (void)addSubview:(UIView *)view { if (view != _wrapperItemView) { [super addSubview:view]; @@ -39,60 +46,95 @@ - (void)addSubview:(UIView *)view { [self refactorViews]; } +- (void)insertHippySubview:(UIView *)view atIndex:(NSInteger)index { + if ([view isKindOfClass:[HippyRefreshWrapperItemView class]]) { + _wrapperItemView = (HippyRefreshWrapperItemView *)view; + } else if ([view conformsToProtocol:@protocol(HippyScrollableProtocol)]) { + _scrollableView = (id)view; + [_scrollableView addScrollListener:self]; + } + [super insertHippySubview:view atIndex:index]; +} + +- (void)invalidate { + [_scrollableView removeScrollListener:self]; +} + + +#pragma mark - Public & Private Methods + - (void)refactorViews { if (_wrapperItemView && _scrollableView) { CGSize size = _wrapperItemView.frame.size; - _wrapperItemView.frame = CGRectMake(0, -size.height, size.width, size.height); + if (self.isHorizontal) { + _wrapperItemView.frame = CGRectMake(-size.width, 0, size.width, size.height); + } else { + _wrapperItemView.frame = CGRectMake(0, -size.height, size.width, size.height); + } [_scrollableView.realScrollView addSubview:_wrapperItemView]; } } - (void)refreshCompleted { - CGFloat duration = _bounceTime != 0 ? _bounceTime : 400; - UIEdgeInsets contentInset = self->_scrollableView.realScrollView.contentInset; - contentInset.top = 0; - [UIView animateWithDuration:duration / 1000.f animations:^{ - [self->_scrollableView.realScrollView setContentInset:contentInset]; + CGFloat duration = _bounceTime != 0 ? _bounceTime : kHippyDefaultRefreshBounceTime; + UIEdgeInsets contentInset = self.scrollableView.realScrollView.contentInset; + if (self.isHorizontal) { + contentInset.left = 0; + } else { + contentInset.top = 0; + } + [UIView animateWithDuration:duration / 1000.0 animations:^{ + [self.scrollableView.realScrollView setContentInset:contentInset]; }]; } - (void)startRefresh { - CGFloat wrapperItemViewHeight = _wrapperItemView.frame.size.height; UIEdgeInsets insets = _scrollableView.realScrollView.contentInset; - insets.top = wrapperItemViewHeight; - CGFloat duration = _bounceTime != 0 ? _bounceTime : 400; - [UIView animateWithDuration:duration / 1000.f animations:^{ - [self->_scrollableView.realScrollView setContentInset:insets]; - [self->_scrollableView.realScrollView setContentOffset:CGPointMake(0, -insets.top)]; + CGPoint targetContentOffset; + if (self.isHorizontal) { + CGFloat wrapperItemViewWidth = CGRectGetWidth(_wrapperItemView.frame); + insets.left = wrapperItemViewWidth; + targetContentOffset = CGPointMake(-wrapperItemViewWidth, 0); + } else { + CGFloat wrapperItemViewHeight = CGRectGetHeight(_wrapperItemView.frame); + insets.top = wrapperItemViewHeight; + targetContentOffset = CGPointMake(0, -wrapperItemViewHeight); + } + + CGFloat duration = _bounceTime > DBL_EPSILON ? _bounceTime : kHippyDefaultRefreshBounceTime; + [UIView animateWithDuration:duration / 1000.0 animations:^{ + [self.scrollableView.realScrollView setContentInset:insets]; + [self.scrollableView.realScrollView setContentOffset:targetContentOffset]; }]; if (_onRefresh) { _onRefresh(@{}); } } -- (void)insertHippySubview:(UIView *)view atIndex:(NSInteger)index { - if ([view isKindOfClass:[HippyRefreshWrapperItemView class]]) { - _wrapperItemView = (HippyRefreshWrapperItemView *)view; - } else if ([view conformsToProtocol:@protocol(HippyScrollableProtocol)]) { - _scrollableView = (id)view; - [_scrollableView addScrollListener:self]; - } - [super insertHippySubview:view atIndex:index]; -} - -- (void)invalidate { - [_scrollableView removeScrollListener:self]; -} +#pragma mark - ScrollListener, UIScrollViewDelegate -- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { - CGFloat wrapperItemViewHeight = _wrapperItemView.frame.size.height; +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView + withVelocity:(CGPoint)velocity + targetContentOffset:(inout CGPoint *)targetContentOffset { UIEdgeInsets insets = scrollView.contentInset; - CGFloat contentOffsetY = scrollView.contentOffset.y; - if (contentOffsetY <= -wrapperItemViewHeight && insets.top != wrapperItemViewHeight) { - insets.top = wrapperItemViewHeight; - scrollView.contentInset = insets; - if (_onRefresh) { - _onRefresh(@{}); + if (self.isHorizontal) { + CGFloat wrapperItemViewWidth = CGRectGetWidth(_wrapperItemView.frame); + CGFloat contentOffsetX = scrollView.contentOffset.x; + if (contentOffsetX <= -wrapperItemViewWidth && insets.left != wrapperItemViewWidth) { + // Update the end sliding state of scrollview + targetContentOffset->x = -wrapperItemViewWidth; + // start refresh and call js + [self startRefresh]; + } + } else { + CGFloat wrapperItemViewHeight = CGRectGetHeight(_wrapperItemView.frame); + CGFloat contentOffsetY = scrollView.contentOffset.y; + if (contentOffsetY <= -wrapperItemViewHeight && insets.top != wrapperItemViewHeight) { + insets.top = wrapperItemViewHeight; + scrollView.contentInset = insets; + if (_onRefresh) { + _onRefresh(@{}); + } } } } diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.m b/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.m index 6cd43a5bd3c..03be58b1de5 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.m +++ b/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.m @@ -21,13 +21,7 @@ */ #import "HippyRefreshWrapperItemView.h" #import "UIView+Hippy.h" -@implementation HippyRefreshWrapperItemView -- (void)setFrame:(CGRect)frame { - if ([self.superview isKindOfClass:[UIScrollView class]]) { - frame.origin.y = -frame.size.height; - } - [super setFrame:frame]; -} +@implementation HippyRefreshWrapperItemView @end diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapperViewManager.m b/ios/sdk/component/refreshview/HippyRefreshWrapperViewManager.m index ac793e9efac..42deb5c3d92 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapperViewManager.m +++ b/ios/sdk/component/refreshview/HippyRefreshWrapperViewManager.m @@ -27,11 +27,13 @@ @implementation HippyRefreshWrapperViewManager HIPPY_EXPORT_MODULE(RefreshWrapper) +HIPPY_EXPORT_VIEW_PROPERTY(horizontal, BOOL) +HIPPY_EXPORT_VIEW_PROPERTY(bounceTime, CGFloat) HIPPY_EXPORT_VIEW_PROPERTY(onRefresh, HippyDirectEventBlock) -HIPPY_EXPORT_VIEW_PROPERTY(bounceTime, CGFloat) - (UIView *)view { - return [HippyRefreshWrapper new]; + HippyRefreshWrapper *refreshWrapper = [HippyRefreshWrapper new]; + return refreshWrapper; } HIPPY_EXPORT_METHOD(refreshComplected:(NSNumber *__nonnull)hippyTag args:(id)arg) { diff --git a/ios/sdk/component/viewPager/HippyViewPager.h b/ios/sdk/component/viewPager/HippyViewPager.h index f4f934c1210..cbd253ea637 100644 --- a/ios/sdk/component/viewPager/HippyViewPager.h +++ b/ios/sdk/component/viewPager/HippyViewPager.h @@ -31,7 +31,7 @@ */ typedef void (^ViewPagerItemsCountChanged)(NSUInteger count); -@interface HippyViewPager : UIScrollView +@interface HippyViewPager : UIScrollView @property (nonatomic, strong) HippyDirectEventBlock onPageSelected; @property (nonatomic, strong) HippyDirectEventBlock onPageScroll; @property (nonatomic, strong) HippyDirectEventBlock onPageScrollStateChanged; diff --git a/ios/sdk/component/viewPager/HippyViewPager.m b/ios/sdk/component/viewPager/HippyViewPager.m index 644412ecd11..f7ca0660416 100644 --- a/ios/sdk/component/viewPager/HippyViewPager.m +++ b/ios/sdk/component/viewPager/HippyViewPager.m @@ -325,7 +325,16 @@ - (void)scrollViewDidEndScrolling { self.previousStopOffset = [self contentOffset].x; } -#pragma mark scrollview listener methods +#pragma mark - scrollview listener methods + +- (UIScrollView *)realScrollView { + return self; +} + +- (NSHashTable *)scrollListeners { + return _scrollViewListener; +} + - (void)addScrollListener:(id)scrollListener { [_scrollViewListener addObject:scrollListener]; } From f602d488e8d46804c6da8143d29fbffe169bd1ee Mon Sep 17 00:00:00 2001 From: wwwcg Date: Mon, 29 Jul 2024 17:34:51 +0800 Subject: [PATCH 2/6] feat(react): refresh wrapper support horizontal and update demo --- .../src/components/ViewPager/index.jsx | 71 ++++++++++++++----- .../src/components/refresh-wrapper.tsx | 6 +- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/examples/hippy-react-demo/src/components/ViewPager/index.jsx b/examples/hippy-react-demo/src/components/ViewPager/index.jsx index affccccf92a..f4579df8b6a 100644 --- a/examples/hippy-react-demo/src/components/ViewPager/index.jsx +++ b/examples/hippy-react-demo/src/components/ViewPager/index.jsx @@ -4,6 +4,7 @@ import { View, Text, ViewPager, + RefreshWrapper, } from '@hippy/react'; import { CirclePagerView, SquarePagerView, TrianglePagerView } from '../../shared/PagerItemView'; @@ -65,6 +66,8 @@ export default class PagerExample extends React.Component { super(props); this.onPageSelected = this.onPageSelected.bind(this); this.onPageScrollStateChanged = this.onPageScrollStateChanged.bind(this); + this.onRefresh = this.onRefresh.bind(this); + this.getRefresh = this.getRefresh.bind(this); } onPageSelected(pageData) { @@ -81,6 +84,25 @@ export default class PagerExample extends React.Component { onPageScroll({ offset, position }) { console.log('onPageScroll', offset, position); } + + onRefresh() { + setTimeout(async () => { + console.log('raytest RefreshWrapper onRefresh'); + this.refresh.refreshCompleted(); + }, 3000); + } + + getRefresh() { + return ( + + + + 刷新中... + + + ); + } + render() { const { selectedIndex } = this.state; return ( @@ -98,26 +120,41 @@ export default class PagerExample extends React.Component { 直接滑到第1页 - { - this.viewpager = ref; + this.refresh = ref; }} - style={styles.container} - initialPage={0} - keyboardDismissMode="none" - scrollEnabled - onPageSelected={this.onPageSelected} - onPageScrollStateChanged={this.onPageScrollStateChanged} - onPageScroll={this.onPageScroll} + style={{ flex: 1 }} + horizontal={true} + onRefresh={this.onRefresh} + bounceTime={500} + getRefresh={this.getRefresh} > - { - [ - SquarePagerView('squarePager'), - TrianglePagerView('TrianglePager'), - CirclePagerView('CirclePager'), - ] - } - + + { + this.viewpager = ref; + }} + style={styles.container} + initialPage={0} + keyboardDismissMode="none" + scrollEnabled + onPageSelected={this.onPageSelected} + onPageScrollStateChanged={this.onPageScrollStateChanged} + onPageScroll={this.onPageScroll} + > + { + [ + SquarePagerView('squarePager'), + TrianglePagerView('TrianglePager'), + CirclePagerView('CirclePager'), + ] + } + + + + { new Array(PAGE_COUNT).fill(0) diff --git a/packages/hippy-react/src/components/refresh-wrapper.tsx b/packages/hippy-react/src/components/refresh-wrapper.tsx index a1067732b0c..b74dffd334a 100644 --- a/packages/hippy-react/src/components/refresh-wrapper.tsx +++ b/packages/hippy-react/src/components/refresh-wrapper.tsx @@ -25,6 +25,7 @@ import Element from '../dom/element-node'; export interface RefreshWrapperProps { bounceTime?: number; + horizontal?: boolean; onRefresh?: () => void; getRefresh?: () => ReactElement; } @@ -63,7 +64,10 @@ export class RefreshWrapper extends React.Component { */ public render() { const { children, ...nativeProps } = this.props; - const style: CSSProperties = { left: 0, right: 0, position: 'absolute' }; + // Set the style according to the horizontal prop + const style: CSSProperties = nativeProps.horizontal + ? { top: 0, bottom: 0, position: 'absolute' } + : { left: 0, right: 0, position: 'absolute' }; return (
{ this.instance = ref; From 4c6bce32357807512d5aa0d5ea53132fff798f19 Mon Sep 17 00:00:00 2001 From: zealotchen Date: Tue, 30 Jul 2024 11:44:50 +0800 Subject: [PATCH 3/6] feat(react): add refresh wrapper footer --- .../src/components/ViewPager/index.jsx | 38 ++++++++++++- .../src/components/refresh-wrapper.tsx | 53 ++++++++++++++++--- 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/examples/hippy-react-demo/src/components/ViewPager/index.jsx b/examples/hippy-react-demo/src/components/ViewPager/index.jsx index f4579df8b6a..7b544bade47 100644 --- a/examples/hippy-react-demo/src/components/ViewPager/index.jsx +++ b/examples/hippy-react-demo/src/components/ViewPager/index.jsx @@ -68,6 +68,8 @@ export default class PagerExample extends React.Component { this.onPageScrollStateChanged = this.onPageScrollStateChanged.bind(this); this.onRefresh = this.onRefresh.bind(this); this.getRefresh = this.getRefresh.bind(this); + this.onFooterRefresh = this.onFooterRefresh.bind(this); + this.getFooterRefresh = this.getFooterRefresh.bind(this); } onPageSelected(pageData) { @@ -85,13 +87,19 @@ export default class PagerExample extends React.Component { console.log('onPageScroll', offset, position); } + /** + * callback for header + */ onRefresh() { setTimeout(async () => { - console.log('raytest RefreshWrapper onRefresh'); + console.log('RefreshWrapper onRefresh'); this.refresh.refreshCompleted(); }, 3000); } + /** + * get header view + */ getRefresh() { return ( @@ -103,6 +111,30 @@ export default class PagerExample extends React.Component { ); } + /** + * callback for footer + */ + onFooterRefresh() { + setTimeout(async () => { + console.log('RefreshWrapper onFooterRefresh'); + this.refresh.refreshFooterCompleted(); + }, 3000); + } + + /** + * get footer view + */ + getFooterRefresh() { + return ( + + + + 刷新中... + + + ); + } + render() { const { selectedIndex } = this.state; return ( @@ -127,9 +159,13 @@ export default class PagerExample extends React.Component { }} style={{ flex: 1 }} horizontal={true} + hiddenHeader={false} + showFooter={true} onRefresh={this.onRefresh} + onFooterRefresh={this.onFooterRefresh} bounceTime={500} getRefresh={this.getRefresh} + getFooterRefresh={this.getFooterRefresh} > void; - getRefresh?: () => ReactElement; + hiddenHeader?: boolean; + showFooter?: boolean; + onRefresh?: () => void; // header refresh callback + getRefresh?: () => ReactElement; // get header refresh view + onFooterRefresh?: () => void; // footer refresh callback + getFooterRefresh?: () => ReactElement; // get footer refresh view } /** @@ -39,26 +43,42 @@ export interface RefreshWrapperProps { export class RefreshWrapper extends React.Component { private instance: Element | Fiber | HTMLDivElement | null = null; private refreshComplected: () => void; + private refreshFooterComplected: () => void; public constructor(props: RefreshWrapperProps) { super(props); this.refreshComplected = this.refreshCompleted.bind(this); + this.refreshFooterComplected = this.refreshFooterCompleted.bind(this); } /** - * Call native for start refresh. + * Call native for start refresh. (For Header) */ public startRefresh() { callUIFunction(this.instance as Element, 'startRefresh', null); } /** - * Call native that data is refreshed + * Call native for start refresh. (For Footer) + */ + public startRefreshFooter() { + callUIFunction(this.instance as Element, 'startRefreshFooter', null); + } + + /** + * Call native that data is refreshed. (For Header) */ public refreshCompleted() { callUIFunction(this.instance as Element, 'refreshComplected', null); } + /** + * Call native that data is refreshed. (For Footer) + */ + public refreshFooterCompleted() { + callUIFunction(this.instance as Element, 'refreshFooterComplected', null); + } + /** * @ignore */ @@ -66,20 +86,26 @@ export class RefreshWrapper extends React.Component { const { children, ...nativeProps } = this.props; // Set the style according to the horizontal prop const style: CSSProperties = nativeProps.horizontal - ? { top: 0, bottom: 0, position: 'absolute' } - : { left: 0, right: 0, position: 'absolute' }; + ? { top: 0, bottom: 0, position: 'absolute' } + : { left: 0, right: 0, position: 'absolute' }; return (
{ this.instance = ref; }} {...nativeProps}> -
+ { !this.props.hiddenHeader ?
{ this.getRefresh() } -
+
: null} { children } + { this.props.showFooter ?
+ { this.getFooterRefresh() } +
: null }
); } + /** + * callback for header + */ private getRefresh(): ReactElement | null { const { getRefresh } = this.props; if (typeof getRefresh === 'function') { @@ -87,6 +113,17 @@ export class RefreshWrapper extends React.Component { } return null; } + + /** + * callback for footer + */ + private getFooterRefresh(): ReactElement | null { + const { getFooterRefresh } = this.props; + if (typeof getFooterRefresh === 'function') { + return getFooterRefresh() || null; + } + return null; + } } export default RefreshWrapper; From 7a07b5a230c07afa55bf74d427ac56ed6f3e2409 Mon Sep 17 00:00:00 2001 From: zealotchen Date: Tue, 30 Jul 2024 20:21:40 +0800 Subject: [PATCH 4/6] feat(react): modify refresh-wrapper refreshFooterCompleted --- packages/hippy-react/src/components/refresh-wrapper.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/hippy-react/src/components/refresh-wrapper.tsx b/packages/hippy-react/src/components/refresh-wrapper.tsx index 47bd5412df7..f04c17e1cf4 100644 --- a/packages/hippy-react/src/components/refresh-wrapper.tsx +++ b/packages/hippy-react/src/components/refresh-wrapper.tsx @@ -42,13 +42,9 @@ export interface RefreshWrapperProps { */ export class RefreshWrapper extends React.Component { private instance: Element | Fiber | HTMLDivElement | null = null; - private refreshComplected: () => void; - private refreshFooterComplected: () => void; public constructor(props: RefreshWrapperProps) { super(props); - this.refreshComplected = this.refreshCompleted.bind(this); - this.refreshFooterComplected = this.refreshFooterCompleted.bind(this); } /** @@ -76,7 +72,7 @@ export class RefreshWrapper extends React.Component { * Call native that data is refreshed. (For Footer) */ public refreshFooterCompleted() { - callUIFunction(this.instance as Element, 'refreshFooterComplected', null); + callUIFunction(this.instance as Element, 'refreshFooterCompleted', null); } /** From 3c6b696fbf872bbb238b06f5d4426bd1e0051d39 Mon Sep 17 00:00:00 2001 From: wwwcg Date: Tue, 30 Jul 2024 20:49:44 +0800 Subject: [PATCH 5/6] feat(ios): add footer support to refreshWrapper --- .../component/listview/HippyBaseListView.m | 30 +++- .../refreshview/HippyRefreshWrapper.h | 9 ++ .../refreshview/HippyRefreshWrapper.m | 140 +++++++++++++++--- .../refreshview/HippyRefreshWrapperItemView.h | 6 + .../refreshview/HippyRefreshWrapperItemView.m | 6 +- .../HippyRefreshWrapperItemViewManager.h | 7 + .../HippyRefreshWrapperItemViewManager.m | 15 ++ .../HippyRefreshWrapperViewManager.m | 18 +++ .../scrollview/HippyScrollableProtocol.h | 45 +++++- ios/sdk/component/viewPager/HippyViewPager.m | 21 +++ 10 files changed, 264 insertions(+), 33 deletions(-) diff --git a/ios/sdk/component/listview/HippyBaseListView.m b/ios/sdk/component/listview/HippyBaseListView.m index 0d4bbc4fc22..3352610b125 100644 --- a/ios/sdk/component/listview/HippyBaseListView.m +++ b/ios/sdk/component/listview/HippyBaseListView.m @@ -33,12 +33,16 @@ @interface HippyBaseListView () +/// Scrollable's scroll event delegates +@property (nonatomic, strong) NSHashTable> *scrollListeners; +/// Scrollable's layout event delegates +@property (nonatomic, strong) NSHashTable> *layoutDelegates; + @end @implementation HippyBaseListView { __weak HippyBridge *_bridge; __weak HippyRootView *_rootView; - NSHashTable *_scrollListeners; BOOL _isInitialListReady; NSUInteger _preNumberOfRows; BOOL _allowNextScrollNoMatterWhat; @@ -55,7 +59,6 @@ @implementation HippyBaseListView { - (instancetype)initWithBridge:(HippyBridge *)bridge { if (self = [super initWithFrame:CGRectZero]) { _bridge = bridge; - _scrollListeners = [NSHashTable weakObjectsHashTable]; _dataSource = [HippyBaseListViewDataSource new]; _isInitialListReady = NO; _preNumberOfRows = 0; @@ -197,6 +200,9 @@ - (void)zoomToRect:(__unused CGRect)rect animated:(__unused BOOL)animated { } - (void)addScrollListener:(NSObject *)scrollListener { + if (!self.scrollListeners) { + self.scrollListeners = [NSHashTable weakObjectsHashTable]; + } [_scrollListeners addObject:scrollListener]; } @@ -231,6 +237,19 @@ - (void)scrollToIndex:(NSInteger)index animated:(BOOL)animated { } } +- (void)addHippyScrollableLayoutDelegate:(id)delegate { + HippyAssertMainThread(); + if (!self.layoutDelegates) { + self.layoutDelegates = [NSHashTable weakObjectsHashTable]; + } + [self.layoutDelegates addObject:delegate]; +} + +- (void)removeHippyScrollableLayoutDelegate:(id)delegate { + HippyAssertMainThread(); + [self.layoutDelegates removeObject:delegate]; +} + #pragma mark - Delegate & Datasource - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { @@ -368,6 +387,13 @@ - (void)tableViewDidLayoutSubviews:(HippyListTableView *)tableView { } } _previousVisibleCells = visibleCells; + + // Notify delegates of HippyScrollableLayoutDelegate + for (id layoutDelegate in self.layoutDelegates) { + if ([layoutDelegate respondsToSelector:@selector(scrollableDidLayout:)]) { + [layoutDelegate scrollableDidLayout:self]; + } + } } #pragma mark - Scroll diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapper.h b/ios/sdk/component/refreshview/HippyRefreshWrapper.h index 85d745756e0..38b898c3436 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapper.h +++ b/ios/sdk/component/refreshview/HippyRefreshWrapper.h @@ -40,12 +40,21 @@ NS_ASSUME_NONNULL_BEGIN /// The onRefresh block that JS side binding. @property (nonatomic, copy) HippyDirectEventBlock onRefresh; +/// The footer onRefresh block that JS side binding. +@property (nonatomic, copy) HippyDirectEventBlock onFooterRefresh; + /// Call to indicate refresh completion. - (void)refreshCompleted; +/// Call to indicate refresh footer completion. +- (void)refreshFooterCompleted; + /// Call to start the refresh process. - (void)startRefresh; +/// Call to start the footer refresh process. +- (void)startRefreshFooter; + @end NS_ASSUME_NONNULL_END diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapper.m b/ios/sdk/component/refreshview/HippyRefreshWrapper.m index dcbee8378f9..ddf669a9300 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapper.m +++ b/ios/sdk/component/refreshview/HippyRefreshWrapper.m @@ -28,10 +28,12 @@ static NSTimeInterval const kHippyDefaultRefreshBounceTime = 400.0; -@interface HippyRefreshWrapper () +@interface HippyRefreshWrapper () -/// The child view of RefreshWrapper -@property (nonatomic, weak) HippyRefreshWrapperItemView *wrapperItemView; +/// The child header view of RefreshWrapper +@property (nonatomic, weak) HippyRefreshWrapperItemView *headerItemView; +/// The child footer view of RefreshWrapper +@property (nonatomic, weak) HippyRefreshWrapperFooterItemView *footerItemView; /// Scrollable target @property (nonatomic, weak) id scrollableView; @@ -40,7 +42,7 @@ @interface HippyRefreshWrapper () @implementation HippyRefreshWrapper - (void)addSubview:(UIView *)view { - if (view != _wrapperItemView) { + if (view != _headerItemView && view != _footerItemView) { [super addSubview:view]; } [self refactorViews]; @@ -48,10 +50,13 @@ - (void)addSubview:(UIView *)view { - (void)insertHippySubview:(UIView *)view atIndex:(NSInteger)index { if ([view isKindOfClass:[HippyRefreshWrapperItemView class]]) { - _wrapperItemView = (HippyRefreshWrapperItemView *)view; + _headerItemView = (HippyRefreshWrapperItemView *)view; } else if ([view conformsToProtocol:@protocol(HippyScrollableProtocol)]) { _scrollableView = (id)view; [_scrollableView addScrollListener:self]; + } else if ([view isKindOfClass:[HippyRefreshWrapperFooterItemView class]]) { + _footerItemView = (HippyRefreshWrapperFooterItemView *)view; + [_scrollableView addHippyScrollableLayoutDelegate:self]; } [super insertHippySubview:view atIndex:index]; } @@ -64,14 +69,14 @@ - (void)invalidate { #pragma mark - Public & Private Methods - (void)refactorViews { - if (_wrapperItemView && _scrollableView) { - CGSize size = _wrapperItemView.frame.size; + if (_headerItemView && _scrollableView) { + CGSize size = _headerItemView.frame.size; if (self.isHorizontal) { - _wrapperItemView.frame = CGRectMake(-size.width, 0, size.width, size.height); + _headerItemView.frame = CGRectMake(-size.width, 0, size.width, size.height); } else { - _wrapperItemView.frame = CGRectMake(0, -size.height, size.width, size.height); + _headerItemView.frame = CGRectMake(0, -size.height, size.width, size.height); } - [_scrollableView.realScrollView addSubview:_wrapperItemView]; + [_scrollableView.realScrollView addSubview:_headerItemView]; } } @@ -88,15 +93,28 @@ - (void)refreshCompleted { }]; } +- (void)refreshFooterCompleted { + CGFloat duration = _bounceTime != 0 ? _bounceTime : kHippyDefaultRefreshBounceTime; + UIEdgeInsets contentInset = self.scrollableView.realScrollView.contentInset; + if (self.isHorizontal) { + contentInset.right = 0; + } else { + contentInset.bottom = 0; + } + [UIView animateWithDuration:duration / 1000.0 animations:^{ + [self.scrollableView.realScrollView setContentInset:contentInset]; + }]; +} + - (void)startRefresh { UIEdgeInsets insets = _scrollableView.realScrollView.contentInset; CGPoint targetContentOffset; if (self.isHorizontal) { - CGFloat wrapperItemViewWidth = CGRectGetWidth(_wrapperItemView.frame); + CGFloat wrapperItemViewWidth = CGRectGetWidth(_headerItemView.frame); insets.left = wrapperItemViewWidth; targetContentOffset = CGPointMake(-wrapperItemViewWidth, 0); } else { - CGFloat wrapperItemViewHeight = CGRectGetHeight(_wrapperItemView.frame); + CGFloat wrapperItemViewHeight = CGRectGetHeight(_headerItemView.frame); insets.top = wrapperItemViewHeight; targetContentOffset = CGPointMake(0, -wrapperItemViewHeight); } @@ -111,6 +129,33 @@ - (void)startRefresh { } } +- (void)startRefreshFooter { + UIScrollView *scrollView = _scrollableView.realScrollView; + UIEdgeInsets insets = scrollView.contentInset; + CGSize contentSize = _scrollableView.contentSize; + CGPoint targetContentOffset; + if (self.isHorizontal) { + CGFloat wrapperItemViewWidth = CGRectGetWidth(_footerItemView.frame); + CGFloat scrollViewWidth = CGRectGetWidth(scrollView.frame); + insets.right = wrapperItemViewWidth; + targetContentOffset = CGPointMake(contentSize.width - scrollViewWidth + wrapperItemViewWidth, 0); + } else { + CGFloat wrapperItemViewHeight = CGRectGetHeight(_footerItemView.frame); + CGFloat scrollViewHeight = CGRectGetHeight(scrollView.frame); + insets.bottom = wrapperItemViewHeight; + targetContentOffset = CGPointMake(0, contentSize.height - scrollViewHeight + wrapperItemViewHeight); + } + + CGFloat duration = _bounceTime > DBL_EPSILON ? _bounceTime : kHippyDefaultRefreshBounceTime; + [UIView animateWithDuration:duration / 1000.0 animations:^{ + [self.scrollableView.realScrollView setContentInset:insets]; + [self.scrollableView.realScrollView setContentOffset:targetContentOffset]; + }]; + if (_onFooterRefresh) { + _onFooterRefresh(@{}); + } +} + #pragma mark - ScrollListener, UIScrollViewDelegate - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView @@ -118,24 +163,71 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView targetContentOffset:(inout CGPoint *)targetContentOffset { UIEdgeInsets insets = scrollView.contentInset; if (self.isHorizontal) { - CGFloat wrapperItemViewWidth = CGRectGetWidth(_wrapperItemView.frame); + // horizontal, for example, wrapping a view pager CGFloat contentOffsetX = scrollView.contentOffset.x; - if (contentOffsetX <= -wrapperItemViewWidth && insets.left != wrapperItemViewWidth) { - // Update the end sliding state of scrollview - targetContentOffset->x = -wrapperItemViewWidth; - // start refresh and call js - [self startRefresh]; + if (_headerItemView) { + CGFloat wrapperItemViewWidth = CGRectGetWidth(_headerItemView.frame); + if (contentOffsetX <= -wrapperItemViewWidth && insets.left != wrapperItemViewWidth) { + // Update the end sliding state of scrollview + targetContentOffset->x = -wrapperItemViewWidth; + // start refresh and call js + [self startRefresh]; + } + } + + if (_footerItemView) { + CGSize contentSize = scrollView.contentSize; + CGFloat scrollViewWidth = CGRectGetWidth(scrollView.frame); + CGFloat footerItemWidth = CGRectGetWidth(_footerItemView.frame); + if (contentOffsetX >= contentSize.width - scrollViewWidth + footerItemWidth && insets.right != footerItemWidth) { + // Update the end sliding state of scrollview + targetContentOffset->x = contentSize.width - scrollViewWidth + footerItemWidth; + // start refresh and call js + [self startRefreshFooter]; + } } + } else { - CGFloat wrapperItemViewHeight = CGRectGetHeight(_wrapperItemView.frame); + // vertical refresh wrapper, for example, wrapping a listview CGFloat contentOffsetY = scrollView.contentOffset.y; - if (contentOffsetY <= -wrapperItemViewHeight && insets.top != wrapperItemViewHeight) { - insets.top = wrapperItemViewHeight; - scrollView.contentInset = insets; - if (_onRefresh) { - _onRefresh(@{}); + if (_headerItemView) { + CGFloat wrapperItemViewHeight = CGRectGetHeight(_headerItemView.frame); + if (contentOffsetY <= -wrapperItemViewHeight && insets.top != wrapperItemViewHeight) { + insets.top = wrapperItemViewHeight; + scrollView.contentInset = insets; + if (_onRefresh) { + _onRefresh(@{}); + } } } + + if (_footerItemView) { + CGFloat wrapperItemViewHeight = CGRectGetHeight(_footerItemView.frame); + CGFloat scrollViewHeight = CGRectGetHeight(scrollView.frame); + if (contentOffsetY >= wrapperItemViewHeight - scrollViewHeight && insets.bottom != wrapperItemViewHeight) { + insets.bottom = wrapperItemViewHeight; + scrollView.contentInset = insets; + if (_onFooterRefresh) { + _onFooterRefresh(@{}); + } + } + } + } +} + +#pragma mark - HippyScrollableLayoutDelegate + +- (void)scrollableDidLayout:(id)scrollableView { + if (_footerItemView && _scrollableView) { + CGSize size = _footerItemView.frame.size; + CGSize contentSize = _scrollableView.realScrollView.contentSize; + + if (self.isHorizontal) { + _footerItemView.frame = CGRectMake(contentSize.width, 0, size.width, size.height); + } else { + _footerItemView.frame = CGRectMake(0, contentSize.height, size.width, size.height); + } + [_scrollableView.realScrollView addSubview:_footerItemView]; } } diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.h b/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.h index e23fab36cb4..c93cd0360b3 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.h +++ b/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.h @@ -22,6 +22,12 @@ #import +/// HeaderItem of RefreshWrapper @interface HippyRefreshWrapperItemView : UIView @end + +/// FooterItem of RefreshWrapper +@interface HippyRefreshWrapperFooterItemView : UIView + +@end diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.m b/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.m index 03be58b1de5..097ce6c9056 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.m +++ b/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.m @@ -19,9 +19,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + #import "HippyRefreshWrapperItemView.h" -#import "UIView+Hippy.h" @implementation HippyRefreshWrapperItemView @end + +@implementation HippyRefreshWrapperFooterItemView + +@end diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapperItemViewManager.h b/ios/sdk/component/refreshview/HippyRefreshWrapperItemViewManager.h index 72ae423e988..86bbb413147 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapperItemViewManager.h +++ b/ios/sdk/component/refreshview/HippyRefreshWrapperItemViewManager.h @@ -24,8 +24,15 @@ NS_ASSUME_NONNULL_BEGIN +/// HeaderItem's ViewManager of RefreshWrapper @interface HippyRefreshWrapperItemViewManager : HippyViewManager @end +/// FooterItem's ViewManager of RefreshWrapper +@interface HippyRefreshWrapperFooterItemViewManager : HippyViewManager + +@end + + NS_ASSUME_NONNULL_END diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapperItemViewManager.m b/ios/sdk/component/refreshview/HippyRefreshWrapperItemViewManager.m index 15c41b24129..cc86ed94a2f 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapperItemViewManager.m +++ b/ios/sdk/component/refreshview/HippyRefreshWrapperItemViewManager.m @@ -22,9 +22,24 @@ #import "HippyRefreshWrapperItemViewManager.h" #import "HippyRefreshWrapperItemView.h" + @implementation HippyRefreshWrapperItemViewManager + HIPPY_EXPORT_MODULE(RefreshWrapperItemView) + - (UIView *)view { return [HippyRefreshWrapperItemView new]; } + +@end + + +@implementation HippyRefreshWrapperFooterItemViewManager + +HIPPY_EXPORT_MODULE(RefreshWrapperFooterItemView) + +- (UIView *)view { + return [HippyRefreshWrapperFooterItemView new]; +} + @end diff --git a/ios/sdk/component/refreshview/HippyRefreshWrapperViewManager.m b/ios/sdk/component/refreshview/HippyRefreshWrapperViewManager.m index 42deb5c3d92..d348e18f1d0 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapperViewManager.m +++ b/ios/sdk/component/refreshview/HippyRefreshWrapperViewManager.m @@ -23,6 +23,8 @@ #import "HippyRefreshWrapperViewManager.h" #import "HippyRefreshWrapper.h" #import "HippyUIManager.h" + + @implementation HippyRefreshWrapperViewManager HIPPY_EXPORT_MODULE(RefreshWrapper) @@ -30,6 +32,7 @@ @implementation HippyRefreshWrapperViewManager HIPPY_EXPORT_VIEW_PROPERTY(horizontal, BOOL) HIPPY_EXPORT_VIEW_PROPERTY(bounceTime, CGFloat) HIPPY_EXPORT_VIEW_PROPERTY(onRefresh, HippyDirectEventBlock) +HIPPY_EXPORT_VIEW_PROPERTY(onFooterRefresh, HippyDirectEventBlock) - (UIView *)view { HippyRefreshWrapper *refreshWrapper = [HippyRefreshWrapper new]; @@ -43,6 +46,13 @@ - (UIView *)view { }]; } +HIPPY_EXPORT_METHOD(refreshFooterCompleted:(NSNumber *__nonnull)hippyTag args:(id)arg) { + [self.bridge.uiManager addUIBlock:^(HippyUIManager *uiManager, NSDictionary *viewRegistry) { + HippyRefreshWrapper *wrapperView = viewRegistry[hippyTag]; + [wrapperView refreshFooterCompleted]; + }]; +} + HIPPY_EXPORT_METHOD(startRefresh:(NSNumber *__nonnull)hippyTag args:(id)arg) { [self.bridge.uiManager addUIBlock:^(HippyUIManager *uiManager, NSDictionary *viewRegistry) { HippyRefreshWrapper *wrapperView = viewRegistry[hippyTag]; @@ -50,4 +60,12 @@ - (UIView *)view { }]; } +HIPPY_EXPORT_METHOD(startRefreshFooter:(NSNumber *__nonnull)hippyTag args:(id)arg) { + [self.bridge.uiManager addUIBlock:^(HippyUIManager *uiManager, NSDictionary *viewRegistry) { + HippyRefreshWrapper *wrapperView = viewRegistry[hippyTag]; + [wrapperView startRefreshFooter]; + }]; +} + + @end diff --git a/ios/sdk/component/scrollview/HippyScrollableProtocol.h b/ios/sdk/component/scrollview/HippyScrollableProtocol.h index 311b90206e1..39fd49d18c4 100644 --- a/ios/sdk/component/scrollview/HippyScrollableProtocol.h +++ b/ios/sdk/component/scrollview/HippyScrollableProtocol.h @@ -22,24 +22,57 @@ #import -#define RN_FORWARD_SCROLL_EVENT(call) \ - for (NSObject * scrollViewListener in [self scrollListeners]) { \ - if ([scrollViewListener respondsToSelector:_cmd]) { \ - [scrollViewListener call]; \ - } \ - } +@protocol HippyScrollableProtocol; +/// Delegate used to deliver layout events +@protocol HippyScrollableLayoutDelegate +/// Trigger when scrollable did layout subviews. +/// - Parameter scrollableView: scrollable object +- (void)scrollableDidLayout:(id)scrollableView; + +@end + + +/// Scrollable components' protocol @protocol HippyScrollableProtocol +/// Return realScrollView's contentSize @property (nonatomic, readonly) CGSize contentSize; +/// Add scroll event listener +/// - Parameter scrollListener: id - (void)addScrollListener:(NSObject *)scrollListener; + +/// Remove scroll event listener +/// - Parameter scrollListener: id - (void)removeScrollListener:(NSObject *)scrollListener; + +/// Get the real scrollView - (UIScrollView *)realScrollView; + +/// Get all scroll event listeners - (NSHashTable *)scrollListeners; @optional + +/// Scroll to specific offset +/// - Parameters: +/// - offset: contentOffset CGPoint +/// - animated: BOOL - (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated; + +/// Scroll to specific index +/// - Parameters: +/// - index: NSInteger +/// - animated: BOOL - (void)scrollToIndex:(NSInteger)index animated:(BOOL)animated; +/// Add layout event delegate +/// - Parameter delegate: id +- (void)addHippyScrollableLayoutDelegate:(id)delegate; + +/// Remove layout delegate +/// - Parameter delegate: id +- (void)removeHippyScrollableLayoutDelegate:(id)delegate; + @end diff --git a/ios/sdk/component/viewPager/HippyViewPager.m b/ios/sdk/component/viewPager/HippyViewPager.m index f7ca0660416..d574b697e7d 100644 --- a/ios/sdk/component/viewPager/HippyViewPager.m +++ b/ios/sdk/component/viewPager/HippyViewPager.m @@ -35,6 +35,7 @@ @interface HippyViewPager () @property (nonatomic, assign) CGRect previousFrame; @property (nonatomic, assign) CGSize previousSize; @property (nonatomic, copy) NSHashTable> *scrollViewListener; +@property (nonatomic, strong) NSHashTable> *layoutDelegates; @property (nonatomic, assign) NSUInteger lastPageIndex; @property (nonatomic, assign) CGFloat targetContentOffsetX; @property (nonatomic, assign) BOOL didFirstTimeLayout; @@ -343,6 +344,19 @@ - (void)removeScrollListener:(id)scrollListener { [_scrollViewListener removeObject:scrollListener]; } +- (void)addHippyScrollableLayoutDelegate:(id)delegate { + HippyAssertMainThread(); + if (!self.layoutDelegates) { + self.layoutDelegates = [NSHashTable weakObjectsHashTable]; + } + [self.layoutDelegates addObject:delegate]; +} + +- (void)removeHippyScrollableLayoutDelegate:(id)delegate { + HippyAssertMainThread(); + [self.layoutDelegates removeObject:delegate]; +} + #pragma mark other methods - (NSUInteger)currentPageIndex { return [self pageIndexForContentOffset:self.contentOffset.x]; @@ -470,6 +484,13 @@ - (void)layoutSubviews { self.needsResetPageIndex= NO; } } + + // Notify delegates of HippyScrollableLayoutDelegate + for (id layoutDelegate in self.layoutDelegates) { + if ([layoutDelegate respondsToSelector:@selector(scrollableDidLayout:)]) { + [layoutDelegate scrollableDidLayout:self]; + } + } } - (NSUInteger)nowPage { From 9cc5c3cc78c4d68f077f385ba75c5afeda19c532 Mon Sep 17 00:00:00 2001 From: wwwcg Date: Thu, 1 Aug 2024 14:54:30 +0800 Subject: [PATCH 6/6] docs(react): add footer to refresh wrapper component --- docs/hippy-react/components.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/hippy-react/components.md b/docs/hippy-react/components.md index d2c6c2224cc..24226ef3b26 100644 --- a/docs/hippy-react/components.md +++ b/docs/hippy-react/components.md @@ -210,18 +210,21 @@ import icon from './qb_icon_new.png'; [[RefreshWrapper 范例]](//github.com/Tencent/Hippy/tree/master/examples/hippy-react-demo/src/components/RefreshWrapper) -包裹住 `ListView` 提供下滑刷新功能的组件. - -> `RefreshWrapper` 现在只支持包裹一个 `ListView` 组件,暂不支持别的组件的下滑刷新功能。 +包裹住 `ListView` 或 `ViewPager` 提供滑动刷新功能的组件. ## 参数 | 参数 | 描述 | 类型 | 支持平台 | | ---------- | ---------------------------------------------------- | ---------- | -------- | -| onRefresh | 当`RefreshWrapper`执行刷新操作时,会触发到此回调函数 | `Function` | `Android、iOS、hippy-react-web、Web-Renderer` | -| getRefresh | 定义刷新栏的视图表现,返回 `View`, `Text` 等组件。 | `Function` | `Android、iOS、hippy-react-web、Web-Renderer` | +| onRefresh | 当`RefreshWrapper`的`Refresh Header`执行刷新操作时,会触发到此回调函数 | `Function` | `Android、iOS、hippy-react-web、Web-Renderer` | +| getRefresh | 定义`Refresh Header`刷新栏的视图表现,返回 `View`, `Text` 等组件。 | `Function` | `Android、iOS、hippy-react-web、Web-Renderer` | | bounceTime | 指定刷新条收回动画的时长,单位为ms | `number` | `Android、iOS、Web-Renderer` | +| hiddenHeader | 是否显示`RefreshWrapper`的`Refresh Header`,`default: false`,`最低支持版本 2.17.6` | `boolean` | `Android、iOS` | +| showFooter | 是否显示`RefreshWrapper`的`Refresh Footer`,`default: false`,`最低支持版本 2.17.6` | `boolean` | `Android、iOS` | +| onFooterRefresh | 当`RefreshWrapper`的`Refresh Footer`执行刷新操作时,会触发到此回调函数。 `最低支持版本 2.17.6` | `Function` | `Android、iOS` | +| getFooterRefresh | 定义`Refresh Footer`刷新栏的视图表现,返回 `View`, `Text` 等组件。`最低支持版本 2.17.6` | `Function` | `Android、iOS` | + ## 方法 ### refreshCompleted