blob: d4b8c5ce5ac7f563e9fab5b13e305c708428f7bb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 "WXScrollerComponent.h"
#import "WXComponent_internal.h"
#import "WXSDKInstance_private.h"
#import "WXComponent.h"
#import "WXDefine.h"
#import "WXConvert.h"
#import "WXSDKInstance.h"
#import "WXUtility.h"
#import "WXLoadingComponent.h"
#import "WXRefreshComponent.h"
#import "WXConfigCenterProtocol.h"
#import "WXSDKEngine.h"
#import "WXComponent+Events.h"
#import "WXPageEventNotifyEvent.h"
#import "WXComponent+Layout.h"
#import "WXUtility.h"
@interface WXScrollerComponentView : UIScrollView
@end
@implementation WXScrollerComponentView
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([(id <WXScrollerProtocol>) self.wx_component respondsToSelector:@selector(requestGestureShouldStopPropagation:shouldReceiveTouch:)]) {
return [(id <WXScrollerProtocol>) self.wx_component requestGestureShouldStopPropagation:gestureRecognizer shouldReceiveTouch:touch];
}
else{
return YES;
}
}
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
[super setContentOffset:contentOffset animated:animated];
BOOL scrollStartEvent = [[self.wx_component valueForKey:@"_scrollStartEvent"] boolValue];
id scrollEventListener = [self.wx_component valueForKey:@"_scrollEventListener"];
if (animated && (scrollStartEvent ||scrollEventListener) && !WXPointEqualToPoint(contentOffset, self.contentOffset)) {
CGFloat scaleFactor = self.wx_component.weexInstance.pixelScaleFactor;
NSDictionary *contentSizeData = @{@"width":@(self.contentSize.width / scaleFactor),
@"height":@(self.contentSize.height / scaleFactor)};
NSDictionary *contentOffsetData = @{@"x":@(-self.contentOffset.x / scaleFactor),
@"y":@(-self.contentOffset.y / scaleFactor)};
if (scrollStartEvent) {
[self.wx_component fireEvent:@"scrollstart" params:@{@"contentSize":contentSizeData, @"contentOffset":contentOffsetData} domChanges:nil];
}
if (scrollEventListener) {
WXScrollerComponent *component = (WXScrollerComponent *)self.wx_component;
component.scrollEventListener(component, @"scrollstart", @{@"contentSize":contentSizeData, @"contentOffset":contentOffsetData});
}
}
}
@end
@interface WXScrollToTarget : NSObject
@property (nonatomic, weak) WXComponent *target;
@property (nonatomic, assign) BOOL hasAppear;
@end
@implementation WXScrollToTarget
@end
@interface WXScrollerComponent()
@property (nonatomic, strong) NSMutableArray * stickyArray;
@property (nonatomic, strong) NSMutableArray * listenerArray;
@property (nonatomic, weak) WXRefreshComponent *refreshComponent;
@property (nonatomic, weak) WXLoadingComponent *loadingComponent;
@end
@implementation WXScrollerComponent
{
CGSize _contentSize;
BOOL _needsPlatformLayout;
BOOL _listenLoadMore;
BOOL _scrollEvent;
BOOL _scrollStartEvent;
BOOL _scrollEndEvent;
BOOL _isScrolling;
BOOL _isDragging;
CGFloat _pageSize;
CGFloat _loadMoreOffset;
CGFloat _previousLoadMoreContentHeight;
CGFloat _offsetAccuracy;
CGPoint _lastContentOffset;
CGPoint _lastScrollEventFiredOffset;
BOOL _scrollable;
NSString * _alwaysScrollableVertical;
NSString * _alwaysScrollableHorizontal;
BOOL _bounces;
// refreshForAppear: load more when refresh component begin appear(if scroll is dragging or decelerating, should delay)
// refreshForWholeVisible: load more until the whole refresh component visible
NSString *_refreshType;
// vertical & horizontal
WXScrollDirection _scrollDirection;
// left & right & up & down
NSString *_direction;
BOOL _showScrollBar;
BOOL _pagingEnabled;
BOOL _shouldNotifiAppearDescendantView;
BOOL _shouldRemoveScrollerListener;
CGPoint _scrollStartPoint;
CGPoint _scrollEndPoint;
//css_node_t *_scrollerCSSNode;
NSHashTable* _delegates;
}
WX_EXPORT_METHOD(@selector(resetLoadmore))
- (void)resetLoadmore
{
_previousLoadMoreContentHeight=0;
}
- (BOOL)_insertSubcomponent:(WXComponent *)subcomponent atIndex:(NSInteger)index
{
BOOL inserted = [super _insertSubcomponent:subcomponent atIndex:index];
if ([subcomponent isKindOfClass:[WXRefreshComponent class]]) {
_refreshComponent = (WXRefreshComponent*)subcomponent;
}
else if ([subcomponent isKindOfClass:[WXLoadingComponent class]]) {
_loadingComponent = (WXLoadingComponent*)subcomponent;
}
// If a vertical list is added to a horizontal scroller, we need platform dependent layout
if (_flexCssNode && [self isKindOfClass:[WXScrollerComponent class]] &&
[subcomponent isKindOfClass:[WXScrollerComponent class]] &&
subcomponent->_positionType != WXPositionTypeFixed &&
(((WXScrollerComponent*)subcomponent).scrollDirection == WXScrollDirectionVertical)) {
if (subcomponent->_flexCssNode) {
if (subcomponent->_flexCssNode->getFlex() > 0 && !isnan(subcomponent->_flexCssNode->getStyleWidth())) {
_needsPlatformLayout = YES;
_flexCssNode->setNeedsPlatformDependentLayout(true);
}
}
}
return inserted;
}
-(instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance
{
self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance];
if (self) {
_stickyArray = [NSMutableArray array];
_listenerArray = [NSMutableArray array];
_scrollEvent = NO;
_scrollStartEvent = NO;
_scrollEndEvent = NO;
_lastScrollEventFiredOffset = CGPointMake(0, 0);
_scrollDirection = attributes[@"scrollDirection"] ? [WXConvert WXScrollDirection:attributes[@"scrollDirection"]] : WXScrollDirectionVertical;
_showScrollBar = attributes[@"showScrollbar"] ? [WXConvert BOOL:attributes[@"showScrollbar"]] : YES;
if (attributes[@"alwaysScrollableVertical"]) {
_alwaysScrollableVertical = [WXConvert NSString:attributes[@"alwaysScrollableVertical"]];
}
if (attributes[@"alwaysScrollableHorizontal"]) {
_alwaysScrollableHorizontal = [WXConvert NSString:attributes[@"alwaysScrollableHorizontal"]];
}
_bounces = attributes[@"bounce"]?[WXConvert BOOL:attributes[@"bounce"]]:YES;
_refreshType = [WXConvert NSString:attributes[@"refreshType"]]?:@"refreshForWholeVisible";
_pagingEnabled = attributes[@"pagingEnabled"] ? [WXConvert BOOL:attributes[@"pagingEnabled"]] : NO;
_pageSize = attributes[@"pageSize"] ? [WXConvert WXPixelType:attributes[@"pageSize"] scaleFactor:self.weexInstance.pixelScaleFactor] : 0;
if (_pageSize < 0) {
_pageSize = 0;
}
_loadMoreOffset = attributes[@"loadmoreoffset"] ? [WXConvert WXPixelType:attributes[@"loadmoreoffset"] scaleFactor:self.weexInstance.pixelScaleFactor] : 0;
_loadmoreretry = attributes[@"loadmoreretry"] ? [WXConvert NSUInteger:attributes[@"loadmoreretry"]] : 0;
_listenLoadMore = [events containsObject:@"loadmore"];
_scrollable = attributes[@"scrollable"] ? [WXConvert BOOL:attributes[@"scrollable"]] : YES;
_offsetAccuracy = attributes[@"offsetAccuracy"] ? [WXConvert WXPixelType:attributes[@"offsetAccuracy"] scaleFactor:self.weexInstance.pixelScaleFactor] : 0;
/* let scroller fill the rest space if it is a child component and has no fixed height & width.
WeexCore also does this in C++, but only for "scroller" and "list" not including for
subclasses of WXScrollerComponent. */
if (_flexCssNode != nullptr) {
if (((_scrollDirection == WXScrollDirectionVertical &&
flexIsUndefined(_flexCssNode->getStyleHeight())) ||
(_scrollDirection == WXScrollDirectionHorizontal &&
flexIsUndefined(_flexCssNode->getStyleWidth()))) &&
_flexCssNode->getFlex() <= 0.0) {
_flexCssNode->set_flex(1.0);
}
}
id configCenter = [WXSDKEngine handlerForProtocol:@protocol(WXConfigCenterProtocol)];
if ([configCenter respondsToSelector:@selector(configForKey:defaultValue:isDefault:)]) {
BOOL shouldNotifiAppearDescendantView = [[configCenter configForKey:@"iOS_weex_ext_config.shouldNotifiAppearDescendantView" defaultValue:@(YES) isDefault:NULL] boolValue];
_shouldNotifiAppearDescendantView = shouldNotifiAppearDescendantView;
BOOL shouldRemoveScrollerListener = [[configCenter configForKey:@"iOS_weex_ext_config.shouldRemoveScrollerListener" defaultValue:@(YES) isDefault:NULL] boolValue];
_shouldRemoveScrollerListener = shouldRemoveScrollerListener;
}
//may be list
if ([@"scroller" isEqualToString:type]) {
[weexInstance.apmInstance updateDiffStats:KEY_PAGE_STATS_SCROLLER_NUM withDiffValue:1];
}
}
return self;
}
- (UIView *)loadView
{
return [[WXScrollerComponentView alloc] init];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self setContentSize:_contentSize];
WXScrollerComponentView *scrollView = (WXScrollerComponentView *)self.view;
scrollView.delegate = self;
scrollView.exclusiveTouch = YES;
scrollView.autoresizesSubviews = NO;
scrollView.clipsToBounds = YES;
scrollView.showsVerticalScrollIndicator = _showScrollBar;
scrollView.showsHorizontalScrollIndicator = _showScrollBar;
scrollView.scrollEnabled = _scrollable;
scrollView.pagingEnabled = _pagingEnabled;
if (scrollView.bounces != _bounces) {
scrollView.bounces = _bounces;
}
if (_alwaysScrollableHorizontal) {
scrollView.alwaysBounceHorizontal = [WXConvert BOOL:_alwaysScrollableHorizontal];
}
if (_alwaysScrollableVertical) {
scrollView.alwaysBounceVertical = [WXConvert BOOL:_alwaysScrollableVertical];
}
if (@available(iOS 11.0, *)) {
if ([scrollView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
} else {
// Fallback on earlier versions
}
if (self.ancestorScroller) {
scrollView.scrollsToTop = NO;
} else {
scrollView.scrollsToTop = YES;
}
if (_pagingEnabled && _pageSize > 0) {
scrollView.pagingEnabled = NO; // turn off system default paging
scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
}
else {
scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
}
}
- (void)layoutDidFinish
{
if ([self isViewLoaded]) {
[self setContentSize:_contentSize];
[self adjustSticky];
[self handleAppear];
}
[_loadingComponent resizeFrame];
}
- (void)_buildViewHierarchyLazily
{
[super _buildViewHierarchyLazily];
[self handleAppear];
}
- (void)viewWillUnload
{
((UIScrollView *)_view).delegate = nil;
}
- (void)dealloc
{
((UIScrollView *)_view).delegate = nil;
[self.stickyArray removeAllObjects];
[self.listenerArray removeAllObjects];
}
- (void)updateAttributes:(NSDictionary *)attributes
{
if (attributes[@"showScrollbar"]) {
_showScrollBar = [WXConvert BOOL:attributes[@"showScrollbar"]];
((UIScrollView *)self.view).showsHorizontalScrollIndicator = _showScrollBar;
((UIScrollView *)self.view).showsVerticalScrollIndicator = _showScrollBar;
}
if (attributes[@"pagingEnabled"]) {
_pagingEnabled = [WXConvert BOOL:attributes[@"pagingEnabled"]];
((UIScrollView *)self.view).pagingEnabled = _pagingEnabled;
}
if (attributes[@"pageSize"]) {
_pageSize = [WXConvert WXPixelType:attributes[@"pageSize"]
scaleFactor:self.weexInstance.pixelScaleFactor];
if (_pageSize < 0) {
_pageSize = 0;
}
}
if ([self isViewLoaded]) {
if (_pagingEnabled && _pageSize > 0) {
((UIScrollView *)self.view).pagingEnabled = NO; // turn off system default paging
((UIScrollView *)self.view).decelerationRate = UIScrollViewDecelerationRateFast;
}
else {
((UIScrollView *)self.view).decelerationRate = UIScrollViewDecelerationRateNormal;
}
}
if (attributes[@"loadmoreoffset"]) {
_loadMoreOffset = [WXConvert WXPixelType:attributes[@"loadmoreoffset"] scaleFactor:self.weexInstance.pixelScaleFactor];
}
if (attributes[@"bounce"]) {
_bounces = [WXConvert BOOL:attributes[@"bounce"]];
((UIScrollView *)self.view).bounces = _bounces;
}
if (attributes[@"loadmoreretry"]) {
NSUInteger loadmoreretry = [WXConvert NSUInteger:attributes[@"loadmoreretry"]];
if (loadmoreretry != _loadmoreretry) {
_previousLoadMoreContentHeight = 0;
}
self.loadmoreretry = loadmoreretry;
}
if (attributes[@"scrollable"]) {
_scrollable = attributes[@"scrollable"] ? [WXConvert BOOL:attributes[@"scrollable"]] : YES;
((UIScrollView *)self.view).scrollEnabled = _scrollable;
}
if (attributes[@"alwaysScrollableHorizontal"]) {
_alwaysScrollableHorizontal = [WXConvert NSString:attributes[@"alwaysScrollableHorizontal"]];
((UIScrollView*)self.view).alwaysBounceHorizontal = [WXConvert BOOL:_alwaysScrollableHorizontal];
}
if (attributes[@"alwaysScrollableVertical"]) {
_alwaysScrollableVertical = [WXConvert NSString:attributes[@"alwaysScrollableVertical"]];
((UIScrollView*)self.view).alwaysBounceVertical = [WXConvert BOOL:_alwaysScrollableVertical];
}
if (attributes[@"refreshType"]) {
_refreshType = [WXConvert NSString:attributes[@"refreshType"]];
}
if (attributes[@"offsetAccuracy"]) {
_offsetAccuracy = [WXConvert WXPixelType:attributes[@"offsetAccuracy"] scaleFactor:self.weexInstance.pixelScaleFactor];
}
if (attributes[@"scrollDirection"]) {
_scrollDirection = attributes[@"scrollDirection"] ? [WXConvert WXScrollDirection:attributes[@"scrollDirection"]] : WXScrollDirectionVertical;
}
}
- (void)addEvent:(NSString *)eventName
{
if ([eventName isEqualToString:@"loadmore"]) {
_listenLoadMore = YES;
}
if ([eventName isEqualToString:@"scroll"]) {
_scrollEvent = YES;
}
if ([eventName isEqualToString:@"scrollstart"]) {
_scrollStartEvent = YES;
}
if ([eventName isEqualToString:@"scrollend"]) {
_scrollEndEvent = YES;
}
}
- (void)removeEvent:(NSString *)eventName
{
if ([eventName isEqualToString:@"loadmore"]) {
_listenLoadMore = NO;
}
if ([eventName isEqualToString:@"scroll"]) {
_scrollEvent = NO;
}
if ([eventName isEqualToString:@"scrollstart"]) {
_scrollStartEvent = NO;
}
if ([eventName isEqualToString:@"scrollend"]) {
_scrollEndEvent = NO;
}
}
#pragma mark WXScrollerProtocol
- (void)addStickyComponent:(WXComponent *)sticky
{
if(![self.stickyArray containsObject:sticky]) {
[self.stickyArray addObject:sticky];
[self adjustSticky];
}
}
- (void)removeStickyComponent:(WXComponent *)sticky
{
if([self.stickyArray containsObject:sticky]) {
[self.stickyArray removeObject:sticky];
WXPerformBlockOnMainThread(^{
[self adjustSticky];
});
}
}
- (void)adjustForRTL
{
if (![WXUtility enableRTLLayoutDirection]) return;
// this is scroll rtl solution.
// scroll layout not use direction, use self tranform
if (self.view && [self isDirectionRTL]) {
if (_transform) {
self.view.layer.transform = CATransform3DConcat(self.view.layer.transform, CATransform3DScale(CATransform3DIdentity, -1, 1, 1));
} else {
self.view.layer.transform = CATransform3DScale(CATransform3DIdentity, -1, 1, 1);
}
} else {
if (!_transform) {
self.view.layer.transform = CATransform3DIdentity;
}
}
}
- (void)_adjustForRTL {
if (![WXUtility enableRTLLayoutDirection]) return;
[super _adjustForRTL];
[self adjustForRTL];
}
- (void)adjustSticky
{
if (![self isViewLoaded]) {
return;
}
CGFloat scrollOffsetY = ((UIScrollView *)self.view).contentOffset.y;
for(WXComponent *component in self.stickyArray) {
if (isnan(component->_absolutePosition.x) && isnan(component->_absolutePosition.y)) {
component->_absolutePosition = [component.supercomponent.view convertPoint:component.view.frame.origin toView:self.view];
}
CGPoint relativePosition = component->_absolutePosition;
if (isnan(relativePosition.y)) {
continue;
}
WXComponent *supercomponent = component.supercomponent;
if(supercomponent != self && component.view.superview != self.view) {
[component.view removeFromSuperview];
[self.view addSubview:component.view];
} else {
[self.view bringSubviewToFront:component.view];
}
CGFloat relativeY = relativePosition.y;
BOOL needSticky = NO;
if (scrollOffsetY >= relativeY) {
needSticky = YES;
} else {
// important: reset views' frame
component.view.frame = CGRectMake(relativePosition.x, relativePosition.y, component.calculatedFrame.size.width, component.calculatedFrame.size.height);
}
if (!needSticky) {
continue;
}
// The minimum Y sticky view can reach is its original position
CGFloat minY = relativeY;
CGPoint superRelativePosition = supercomponent == self ? CGPointZero : [supercomponent.supercomponent.view convertPoint:supercomponent.view.frame.origin toView:self.view];
CGFloat maxY = superRelativePosition.y + supercomponent.calculatedFrame.size.height - component.calculatedFrame.size.height;
CGFloat stickyY = scrollOffsetY;
if (stickyY < minY) {
stickyY = minY;
} else if (stickyY > maxY && ![supercomponent conformsToProtocol:@protocol(WXScrollerProtocol)]) {
// Sticky component can not go beyond its parent's bounds when its parent is not scroller;
stickyY = maxY;
}
UIView *stickyView = component.view;
CGPoint origin = stickyView.frame.origin;
origin.y = stickyY;
stickyView.frame = (CGRect){origin,stickyView.frame.size};
}
}
- (void)addScrollToListener:(WXComponent *)target
{
BOOL has = NO;
NSMutableArray *listenerArray = [self.listenerArray copy];
for (WXScrollToTarget *targetData in listenerArray) {
if (targetData.target == target) {
has = YES;
break;
}
}
if (!has) {
WXScrollToTarget *scrollTarget = [[WXScrollToTarget alloc] init];
scrollTarget.target = target;
scrollTarget.hasAppear = NO;
[self.listenerArray addObject:scrollTarget];
}
}
- (void)removeScrollToListener:(WXComponent *)target
{
if (_shouldRemoveScrollerListener) {
WXScrollToTarget *targetData = nil;
NSMutableArray *listenerArray = [self.listenerArray copy];
for (WXScrollToTarget *targetDataTemp in listenerArray) {
if (targetDataTemp.target == target) {
targetData = targetDataTemp;
break;
}
}
if(targetData) {
[self.listenerArray removeObject:targetData];
}
}
}
- (void)scrollToComponent:(WXComponent *)component withOffset:(CGFloat)offset animated:(BOOL)animated
{
UIScrollView *scrollView = (UIScrollView *)self.view;
// http://dotwe.org/vue/aa1af34e5fc745c0f1520e346904682a
// ignore scroll action if contentSize smaller than scroller frame
if (_scrollDirection == WXScrollDirectionVertical && scrollView.contentSize.height < scrollView.frame.size.height) {
return;
}
if (_scrollDirection == WXScrollDirectionHorizontal && scrollView.contentSize.width < scrollView.frame.size.width) {
return;
}
CGPoint contentOffset = scrollView.contentOffset;
CGFloat scaleFactor = self.weexInstance.pixelScaleFactor;
if (_scrollDirection == WXScrollDirectionHorizontal) {
CGFloat contentOffetX = 0;
if (self.isDirectionRTL && component.supercomponent != self) {
contentOffetX = [component.supercomponent.view convertPoint:CGPointMake(CGRectGetMaxX(component.view.frame), component.view.frame.origin.y) toView:self.view].x;
} else {
contentOffetX = [component.supercomponent.view convertPoint:component.view.frame.origin toView:self.view].x;
}
contentOffetX += offset * scaleFactor;
if (scrollView.contentSize.width >= scrollView.frame.size.width && contentOffetX > scrollView.contentSize.width - scrollView.frame.size.width) {
contentOffset.x = scrollView.contentSize.width - scrollView.frame.size.width;
} else {
contentOffset.x = contentOffetX;
}
} else {
CGFloat contentOffetY = [component.supercomponent.view convertPoint:component.view.frame.origin toView:self.view].y;
contentOffetY += offset * scaleFactor;
if (scrollView.contentSize.height >= scrollView.frame.size.height && contentOffetY > scrollView.contentSize.height - scrollView.frame.size.height) {
contentOffset.y = scrollView.contentSize.height - scrollView.frame.size.height;
} else {
contentOffset.y = contentOffetY;
}
}
[scrollView setContentOffset:contentOffset animated:animated];
}
- (BOOL)isNeedLoadMore
{
if (WXScrollDirectionVertical == _scrollDirection) {
if (_loadMoreOffset >= 0.0 && ((UIScrollView *)self.view).contentOffset.y >= 0) {
return _previousLoadMoreContentHeight != ((UIScrollView *)self.view).contentSize.height && ((UIScrollView *)self.view).contentSize.height - ((UIScrollView *)self.view).contentOffset.y - self.view.frame.size.height <= _loadMoreOffset;
}
} else if (WXScrollDirectionHorizontal == _scrollDirection) {
if (_loadMoreOffset >= 0.0 && ((UIScrollView *)self.view).contentOffset.x >= 0) {
return _previousLoadMoreContentHeight != ((UIScrollView *)self.view).contentSize.width && ((UIScrollView *)self.view).contentSize.width - ((UIScrollView *)self.view).contentOffset.x - self.view.frame.size.width <= _loadMoreOffset;
}
}
return NO;
}
- (void)loadMore
{
[self fireEvent:@"loadmore" params:nil];
if (WXScrollDirectionVertical == _scrollDirection) {
_previousLoadMoreContentHeight = ((UIScrollView *)self.view).contentSize.height;
} else if (WXScrollDirectionHorizontal == _scrollDirection) {
_previousLoadMoreContentHeight = ((UIScrollView *)self.view).contentSize.width;
}
}
- (CGPoint)contentOffset
{
CGPoint rtv = CGPointZero;
UIScrollView *scrollView = (UIScrollView *)self.view;
if (scrollView) {
rtv = scrollView.contentOffset;
}
return rtv;
}
- (void)setContentOffset:(CGPoint)contentOffset
{
UIScrollView *scrollView = (UIScrollView *)self.view;
[scrollView setContentOffset:contentOffset];
}
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
UIScrollView *scrollView = (UIScrollView *)self.view;
[scrollView setContentOffset:contentOffset animated:animated];
}
- (CGSize)contentSize
{
return ((UIScrollView *)self.view).contentSize;
}
- (void)setContentSize:(CGSize)size
{
UIScrollView *scrollView = (UIScrollView *)self.view;
scrollView.contentSize = size;
}
- (UIEdgeInsets)contentInset
{
UIEdgeInsets rtv = UIEdgeInsetsZero;
UIScrollView *scrollView = (UIScrollView *)self.view;
if (scrollView) {
rtv = scrollView.contentInset;
}
return rtv;
}
- (void)setContentInset:(UIEdgeInsets)contentInset
{
UIScrollView *scrollView = (UIScrollView *)self.view;
[scrollView setContentInset:contentInset];
}
- (void)addScrollDelegate:(id<UIScrollViewDelegate>)delegate
{
if (delegate) {
if (!_delegates) {
_delegates = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory];
}
[_delegates addObject:delegate];
}
}
- (void)removeScrollDelegate:(id<UIScrollViewDelegate>)delegate
{
if (delegate) {
[_delegates removeObject:delegate];
}
}
- (WXScrollDirection)scrollDirection
{
return _scrollDirection;
}
- (NSString*)refreshType
{
return _refreshType;
}
- (BOOL)requestGestureShouldStopPropagation:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
return [self gestureShouldStopPropagation:gestureRecognizer shouldReceiveTouch:touch];
}
#pragma mark UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
_isDragging = YES;
if ([_refreshType isEqualToString:@"refreshForAppear"] && _refreshComponent) {
[_refreshComponent setIndicatorHidden:NO];
}
_scrollStartPoint = scrollView.contentOffset;
if (_scrollStartEvent || _scrollEventListener) {
CGFloat scaleFactor = self.weexInstance.pixelScaleFactor;
NSDictionary *contentSizeData = @{@"width":@(scrollView.contentSize.width / scaleFactor),
@"height":@(scrollView.contentSize.height / scaleFactor)};
NSDictionary *contentOffsetData = @{@"x":@(-scrollView.contentOffset.x / scaleFactor),
@"y":@(-scrollView.contentOffset.y / scaleFactor)};
NSDictionary *params = @{@"contentSize":contentSizeData, @"contentOffset":contentOffsetData, @"isDragging":@(scrollView.isDragging)};
if (_scrollStartEvent) {
[self fireEvent:@"scrollstart" params:params domChanges:nil];
}
if (_scrollEventListener) {
_scrollEventListener(self, @"scrollstart", params);
}
}
NSHashTable *delegates = [_delegates copy];
for (id<UIScrollViewDelegate> delegate in delegates) {
if ([delegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
[delegate scrollViewWillBeginDragging:scrollView];
}
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//apply block which are registered
WXSDKInstance *instance = self.weexInstance;
if ([self.ref isEqualToString:WX_SDK_ROOT_REF] &&
[self isKindOfClass:[WXScrollerComponent class]]) {
if (instance.onScroll) {
instance.onScroll(scrollView.contentOffset);
}
}
if (_lastContentOffset.x > scrollView.contentOffset.x) {
_direction = @"right";
} else if (_lastContentOffset.x < scrollView.contentOffset.x) {
_direction = @"left";
if (WXScrollDirectionHorizontal == _scrollDirection) {
[self handleLoadMore];
}
} else if(_lastContentOffset.y > scrollView.contentOffset.y) {
_direction = @"down";
} else if(_lastContentOffset.y < scrollView.contentOffset.y) {
_direction = @"up";
if (WXScrollDirectionVertical == _scrollDirection) {
[self handleLoadMore];
}
}
CGFloat scaleFactor = self.weexInstance.pixelScaleFactor;
if (_isDragging) {
// only trigger pullingDown event when user is dragging
[_refreshComponent pullingdown:@{
REFRESH_DISTANCE_Y: @(fabs((scrollView.contentOffset.y - _lastContentOffset.y)/scaleFactor)),
REFRESH_VIEWHEIGHT: @(_refreshComponent.view.frame.size.height/scaleFactor),
REFRESH_PULLINGDISTANCE: @(fabs(scrollView.contentOffset.y/scaleFactor)),
@"type":@"pullingdown"
}];
}
_lastContentOffset = scrollView.contentOffset;
// check sticky
[self adjustSticky];
[self handleAppear];
if (self.onScroll) {
self.onScroll(scrollView);
}
if (_scrollEvent || _scrollEventListener) {
CGFloat distance = 0;
if (_scrollDirection == WXScrollDirectionHorizontal) {
distance = scrollView.contentOffset.x - _lastScrollEventFiredOffset.x;
} else {
distance = scrollView.contentOffset.y - _lastScrollEventFiredOffset.y;
}
if (fabs(distance) >= _offsetAccuracy) {
NSDictionary *contentSizeData = @{@"width": @(scrollView.contentSize.width / scaleFactor),
@"height": @(scrollView.contentSize.height / scaleFactor)};
//contentOffset values are replaced by (-contentOffset.x,-contentOffset.y), in order to be consistent with Android client.
NSDictionary *contentOffsetData = @{@"x": @(-scrollView.contentOffset.x / scaleFactor),
@"y": @(-scrollView.contentOffset.y / scaleFactor)};
if (_scrollEvent) {
[self fireEvent:@"scroll" params:@{@"contentSize":contentSizeData, @"contentOffset":contentOffsetData} domChanges:nil];
}
if (_scrollEventListener) {
_scrollEventListener(self, @"scroll", @{@"contentSize":contentSizeData, @"contentOffset":contentOffsetData});
}
_lastScrollEventFiredOffset = scrollView.contentOffset;
}
}
NSHashTable *delegates = [_delegates copy];
for (id<UIScrollViewDelegate> delegate in delegates) {
if ([delegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
[delegate scrollViewDidScroll:scrollView];
}
}
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
[self dispatchScrollEndEvent:scrollView];
UIEdgeInsets inset = [scrollView contentInset];
// currently only set contentInset when loading
// if ([_refreshComponent displayState]) {
// inset.top = _refreshComponent.view.frame.size.height;
// }
// else {
// inset.top = 0;
// }
if ([_loadingComponent displayState]) {
inset.bottom = _loadingComponent.view.frame.size.height;
} else {
inset.bottom = 0;
}
[scrollView setContentInset:inset];
NSHashTable *delegates = [_delegates copy];
for (id<UIScrollViewDelegate> delegate in delegates) {
if ([delegate respondsToSelector:@selector(scrollViewDidEndScrollingAnimation:)]) {
[delegate scrollViewDidEndScrollingAnimation:scrollView];
}
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (!_isScrolling) {
[self dispatchScrollEndEvent:scrollView];
_scrollEndPoint = scrollView.contentOffset;
id<WXPageEventNotifyEventProtocol> eventNotify = [WXSDKEngine handlerForProtocol:@protocol(WXPageEventNotifyEventProtocol)];
if ([eventNotify respondsToSelector:@selector(notifyScrollEvent:from:to:)]) {
[eventNotify notifyScrollEvent:self.weexInstance.instanceId from:_scrollStartPoint to:_scrollEndPoint];
}
}
NSHashTable *delegates = [_delegates copy];
for (id<UIScrollViewDelegate> delegate in delegates) {
if ([delegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]) {
[delegate scrollViewDidEndDecelerating:scrollView];
}
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
// Page stop effect
if (_pagingEnabled && _pageSize > 0) {
if (_scrollDirection == WXScrollDirectionVertical) {
CGFloat targetY = scrollView.contentOffset.y + velocity.y * 120.0;
CGFloat targetIndex = round(targetY / _pageSize);
/*
When user's finger departs from screen with any velocity (like swipe gesture).
We make sure that target index is changed.
*/
CGFloat sourceIndex = round(_scrollStartPoint.y / _pageSize);
if (velocity.y > 0.3) {
if (targetIndex <= sourceIndex) {
targetIndex = sourceIndex + 1;
}
}
else if (velocity.y < -0.3) {
if (targetIndex >= sourceIndex) {
targetIndex = sourceIndex - 1;
}
}
if (targetIndex < 0)
targetIndex = 0;
targetContentOffset->y = targetIndex * _pageSize;
}
else {
CGFloat targetX = scrollView.contentOffset.x + velocity.x * 120.0;
CGFloat targetIndex = round(targetX / _pageSize);
/*
When user's finger departs from screen with any velocity (like swipe gesture).
We make sure that target index is changed.
*/
CGFloat sourceIndex = round(_scrollStartPoint.x / _pageSize);
if (velocity.x > 0.3) {
if (targetIndex <= sourceIndex) {
targetIndex = sourceIndex + 1;
}
}
else if (velocity.x < -0.3) {
if (targetIndex >= sourceIndex) {
targetIndex = sourceIndex - 1;
}
}
if (targetIndex < 0)
targetIndex = 0;
targetContentOffset->x = targetIndex * _pageSize;
}
}
if ([_refreshType isEqualToString:@"refreshForAppear"]) {
if(targetContentOffset == nil)
return;
CGPoint offset = *targetContentOffset;
if(velocity.y <= 0) {
// drop down
if( offset.y <= _refreshComponent.calculatedFrame.size.height ) {
[self loadMoreIfNeed];
}
} else if (velocity.y > 0) {
// drop up
}
}
NSHashTable *delegates = [_delegates copy];
for (id<UIScrollViewDelegate> delegate in delegates) {
if ([delegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) {
[delegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset];
}
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
[_loadingComponent.view setHidden:NO];
[_refreshComponent.view setHidden:NO];
//refresh
if ([_refreshType isEqualToString:@"refreshForWholeVisible"]) {
if (_refreshComponent && scrollView.contentOffset.y < 0 && scrollView.contentOffset.y + _refreshComponent.calculatedFrame.size.height < _refreshComponent.calculatedFrame.origin.y) {
[_refreshComponent refresh];
}
}
//loading
if (_loadingComponent && scrollView.contentOffset.y > 0 &&
scrollView.contentOffset.y + scrollView.frame.size.height > _loadingComponent.view.frame.origin.y + _loadingComponent.calculatedFrame.size.height) {
[_loadingComponent loading];
}
if (!decelerate) {
_isScrolling = NO;
[self performSelector:@selector(scrollViewDidEndDecelerating:) withObject:scrollView afterDelay:0.1];
}
NSHashTable *delegates = [_delegates copy];
for (id<UIScrollViewDelegate> delegate in delegates) {
if ([delegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) {
[delegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
}
}
_isDragging = NO;
}
- (void)loadMoreIfNeed
{
WXScrollerComponentView* scrollView = (WXScrollerComponentView *)self.view;
if (scrollView.isDragging || scrollView.isTracking || scrollView.isDecelerating) {
[self performSelector:@selector(loadMoreIfNeed) withObject:nil afterDelay:0.1];
return;
}
[_refreshComponent refresh];
}
- (void)handleAppear
{
if (![self isViewLoaded]) {
return;
}
UIScrollView *scrollView = (UIScrollView *)self.view;
CGFloat vx = scrollView.contentInset.left + scrollView.contentOffset.x;
CGFloat vy = scrollView.contentInset.top + scrollView.contentOffset.y;
CGFloat vw = scrollView.frame.size.width - scrollView.contentInset.left - scrollView.contentInset.right;
CGFloat vh = scrollView.frame.size.height - scrollView.contentInset.top - scrollView.contentInset.bottom;
CGRect scrollRect = CGRectMake(vx, vy, vw, vh);;
// notify action for appear
NSArray *listenerArrayCopy = [self.listenerArray copy];
for(WXScrollToTarget *target in listenerArrayCopy){
if (_shouldNotifiAppearDescendantView) {
// if target component is descendant of scrollerview, it should notify the appear event handler, or here will skip this appear calculation.
if ([target.target isViewLoaded] && [target.target.view isDescendantOfView:self.view]) {
[self scrollToTarget:target scrollRect:scrollRect];
}
} else {
[self scrollToTarget:target scrollRect:scrollRect];
}
}
}
- (CGPoint)absolutePositionForComponent:(WXComponent *)component
{
return [component->_view.superview convertPoint:component->_view.frame.origin toView:_view];
}
#pragma mark Private Methods
- (void)dispatchScrollEndEvent:(UIScrollView *)scrollView
{
if (_scrollEndEvent || _scrollEventListener) {
CGFloat scaleFactor = self.weexInstance.pixelScaleFactor;
NSDictionary *contentSizeData = @{@"width":@(scrollView.contentSize.width / scaleFactor),
@"height":@(scrollView.contentSize.height / scaleFactor)};
NSDictionary *contentOffsetData = @{@"x":@(-scrollView.contentOffset.x / scaleFactor),
@"y":@(-scrollView.contentOffset.y / scaleFactor)};
if (_scrollEndEvent) {
[self fireEvent:@"scrollend" params:@{@"contentSize":contentSizeData, @"contentOffset":contentOffsetData} domChanges:nil];
}
if (_scrollEventListener) {
_scrollEventListener(self, @"scrollend", @{@"contentSize":contentSizeData, @"contentOffset":contentOffsetData});
}
}
}
- (void)scrollToTarget:(WXScrollToTarget *)target scrollRect:(CGRect)rect
{
WXComponent *component = target.target;
if (![component isViewLoaded]) {
return;
}
CGFloat ctop;
if (component && component->_view && component->_view.superview) {
ctop = [self absolutePositionForComponent:component].y;
} else {
ctop = 0.0;
}
CGFloat cbottom = ctop + CGRectGetHeight(component.calculatedFrame);
CGFloat cleft;
if (component && component->_view && component->_view.superview) {
cleft = [self absolutePositionForComponent:component].x;
} else {
cleft = 0.0;
}
CGFloat cright = cleft + CGRectGetWidth(component.calculatedFrame);
CGFloat vtop = CGRectGetMinY(rect), vbottom = CGRectGetMaxY(rect), vleft = CGRectGetMinX(rect), vright = CGRectGetMaxX(rect);
if(cbottom > vtop && ctop <= vbottom && cleft <= vright && cright > vleft){
if(!target.hasAppear && component){
target.hasAppear = YES;
if (component->_appearEvent) {
// NSLog(@"appear:%@, %.2f", component, ctop);
[component fireEvent:@"appear" params:_direction ? @{@"direction":_direction} : nil];
}
}
} else {
if(target.hasAppear && component){
target.hasAppear = NO;
if(component->_disappearEvent){
// NSLog(@"disappear:%@", component);
[component fireEvent:@"disappear" params:_direction ? @{@"direction":_direction} : nil];
}
}
}
}
- (void)handleLoadMore
{
if (_listenLoadMore && [self isNeedLoadMore]) {
[self loadMore];
}
}
#pragma mark Layout
- (CGFloat)_getInnerContentMainSize
{
if (_scrollDirection == WXScrollDirectionVertical) {
return _contentSize.height;
}
else if (_scrollDirection == WXScrollDirectionHorizontal) {
return _contentSize.width;
}
else {
return -1.0f;
}
}
- (void)_assignInnerContentMainSize:(CGFloat)value
{
if (_scrollDirection == WXScrollDirectionVertical) {
_contentSize.height = value;
}
else if (_scrollDirection == WXScrollDirectionHorizontal) {
_contentSize.width = value;
}
}
- (void)_layoutPlatform
{
/* Handle multiple vertical scrollers inside horizontal scroller case. In weexcore,
a verticall list with NAN height will be set flex=1, which suppresses its style-width property.
This will cause two lists with style-width 750px in a horizontal scroller sharing one screen width.
Here we respect its style-width property so that the two lists will both be screen width wide. */
if (_needsPlatformLayout) {
if (_flexCssNode) {
float top = _flexCssNode->getLayoutPositionTop();
float left = _flexCssNode->getLayoutPositionLeft();
float width = _flexCssNode->getLayoutWidth();
float height = _flexCssNode->getLayoutHeight();
if (_scrollDirection == WXScrollDirectionVertical) {
_flexCssNode->setFlexDirection(WeexCore::kFlexDirectionColumn, NO);
_flexCssNode->setStyleWidth(_flexCssNode->getLayoutWidth(), NO);
_flexCssNode->setStyleHeight(FlexUndefined);
} else {
_flexCssNode->setFlexDirection(WeexCore::kFlexDirectionRow, NO);
_flexCssNode->setStyleHeight(_flexCssNode->getLayoutHeight());
_flexCssNode->setStyleWidth(FlexUndefined, NO);
}
_flexCssNode->markAllDirty();
// this is scroll rtl solution.
// scroll layout not use direction, use self tranform
// but we need inherit direction in CSS, so we set children layout diretion manually
_flexCssNode->determineChildLayoutDirection(_flexCssNode->getLayoutDirectionFromPathNode());
std::pair<float, float> renderPageSize;
renderPageSize.first = self.weexInstance.frame.size.width;
renderPageSize.second = self.weexInstance.frame.size.height;
auto parent = _flexCssNode->getParent(); // clear parent temporarily
_flexCssNode->setParent(nullptr, _flexCssNode);
_flexCssNode->calculateLayout(renderPageSize);
_flexCssNode->setParent(parent, _flexCssNode);
// set origin and size back
_flexCssNode->rewriteLayoutResult(left, top, width, height);
}
}
else {
// should not happen, set platform layout to false
_flexCssNode->setNeedsPlatformDependentLayout(false);
}
}
@end