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 diff --git a/examples/hippy-react-demo/src/components/ViewPager/index.jsx b/examples/hippy-react-demo/src/components/ViewPager/index.jsx index affccccf92a..7b544bade47 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,10 @@ 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); + this.onFooterRefresh = this.onFooterRefresh.bind(this); + this.getFooterRefresh = this.getFooterRefresh.bind(this); } onPageSelected(pageData) { @@ -81,6 +86,55 @@ export default class PagerExample extends React.Component { onPageScroll({ offset, position }) { console.log('onPageScroll', offset, position); } + + /** + * callback for header + */ + onRefresh() { + setTimeout(async () => { + console.log('RefreshWrapper onRefresh'); + this.refresh.refreshCompleted(); + }, 3000); + } + + /** + * get header view + */ + getRefresh() { + return ( + + + + 刷新中... + + + ); + } + + /** + * 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 ( @@ -98,26 +152,45 @@ 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} + hiddenHeader={false} + showFooter={true} + onRefresh={this.onRefresh} + onFooterRefresh={this.onFooterRefresh} + bounceTime={500} + getRefresh={this.getRefresh} + getFooterRefresh={this.getFooterRefresh} > - { - [ - 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/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 16ee607e5d1..38b898c3436 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapper.h +++ b/ios/sdk/component/refreshview/HippyRefreshWrapper.h @@ -21,12 +21,40 @@ */ #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; + +/// 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 7898cb28d4e..ddf669a9300 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapper.m +++ b/ios/sdk/component/refreshview/HippyRefreshWrapper.m @@ -24,76 +24,210 @@ #import "UIView+Hippy.h" #import "HippyRefreshWrapperItemView.h" #import "HippyScrollableProtocol.h" -@interface HippyRefreshWrapper () -@property (nonatomic, weak) HippyRefreshWrapperItemView *wrapperItemView; + + +static NSTimeInterval const kHippyDefaultRefreshBounceTime = 400.0; + +@interface HippyRefreshWrapper () + +/// 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; -@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) { + if (view != _headerItemView && view != _footerItemView) { [super addSubview:view]; } [self refactorViews]; } +- (void)insertHippySubview:(UIView *)view atIndex:(NSInteger)index { + if ([view isKindOfClass:[HippyRefreshWrapperItemView class]]) { + _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]; +} + +- (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); - [_scrollableView.realScrollView addSubview:_wrapperItemView]; + if (_headerItemView && _scrollableView) { + CGSize size = _headerItemView.frame.size; + if (self.isHorizontal) { + _headerItemView.frame = CGRectMake(-size.width, 0, size.width, size.height); + } else { + _headerItemView.frame = CGRectMake(0, -size.height, size.width, size.height); + } + [_scrollableView.realScrollView addSubview:_headerItemView]; } } - (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)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 { - 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(_headerItemView.frame); + insets.left = wrapperItemViewWidth; + targetContentOffset = CGPointMake(-wrapperItemViewWidth, 0); + } else { + CGFloat wrapperItemViewHeight = CGRectGetHeight(_headerItemView.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]; +- (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(@{}); } - [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) { + // horizontal, for example, wrapping a view pager + CGFloat contentOffsetX = scrollView.contentOffset.x; + 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 { + // vertical refresh wrapper, for example, wrapping a listview + CGFloat contentOffsetY = scrollView.contentOffset.y; + 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 6cd43a5bd3c..097ce6c9056 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.m +++ b/ios/sdk/component/refreshview/HippyRefreshWrapperItemView.m @@ -19,15 +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 -- (void)setFrame:(CGRect)frame { - if ([self.superview isKindOfClass:[UIScrollView class]]) { - frame.origin.y = -frame.size.height; - } - [super setFrame:frame]; -} +@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 ac793e9efac..d348e18f1d0 100644 --- a/ios/sdk/component/refreshview/HippyRefreshWrapperViewManager.m +++ b/ios/sdk/component/refreshview/HippyRefreshWrapperViewManager.m @@ -23,15 +23,20 @@ #import "HippyRefreshWrapperViewManager.h" #import "HippyRefreshWrapper.h" #import "HippyUIManager.h" + + @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(onFooterRefresh, 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) { @@ -41,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]; @@ -48,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.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..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; @@ -325,7 +326,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]; } @@ -334,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]; @@ -461,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 { diff --git a/packages/hippy-react/src/components/refresh-wrapper.tsx b/packages/hippy-react/src/components/refresh-wrapper.tsx index a1067732b0c..f04c17e1cf4 100644 --- a/packages/hippy-react/src/components/refresh-wrapper.tsx +++ b/packages/hippy-react/src/components/refresh-wrapper.tsx @@ -25,8 +25,13 @@ import Element from '../dom/element-node'; export interface RefreshWrapperProps { bounceTime?: number; - onRefresh?: () => void; - getRefresh?: () => ReactElement; + horizontal?: boolean; + 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 } /** @@ -37,45 +42,66 @@ export interface RefreshWrapperProps { */ export class RefreshWrapper extends React.Component { private instance: Element | Fiber | HTMLDivElement | null = null; - private refreshComplected: () => void; public constructor(props: RefreshWrapperProps) { super(props); - this.refreshComplected = this.refreshCompleted.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, 'refreshFooterCompleted', null); + } + /** * @ignore */ 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; }} {...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') { @@ -83,6 +109,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;