blob: 5bd794b3d8b8722fe6ab005a03c7bab9696b652b [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 "WXCycleSliderComponent.h"
#import "WXIndicatorComponent.h"
#import "WXComponent_internal.h"
#import "NSTimer+Weex.h"
#import "WXSDKManager.h"
#import "WXUtility.h"
#import "WXComponent+Layout.h"
#import "WXComponent+Events.h"
typedef NS_ENUM(NSInteger, Direction) {
DirectionNone = 1 << 0,
DirectionLeft = 1 << 1,
DirectionRight = 1 << 2
};
@class WXRecycleSliderView;
@class WXIndicatorView;
@protocol WXRecycleSliderViewDelegate <UIScrollViewDelegate>
- (void)recycleSliderView:(WXRecycleSliderView *)recycleSliderView didScroll:(UIScrollView *)scrollView;
- (void)recycleSliderView:(WXRecycleSliderView *)recycleSliderView didScrollToItemAtIndex:(NSInteger)index;
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (BOOL)requestGestureShouldStopPropagation:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
@end
@interface WXRecycleSliderView : UIView <UIScrollViewDelegate>
@property (nonatomic, strong) WXIndicatorView *indicator;
@property (nonatomic, weak) id<WXRecycleSliderViewDelegate> delegate;
@property (nonatomic, strong) WXRecycleSliderScrollView *scrollView;
@property (nonatomic, strong) NSMutableArray *itemViews;
@property (nonatomic, assign) Direction direction;
@property (nonatomic, assign) NSInteger currentIndex;
@property (nonatomic, assign) NSInteger nextIndex;
@property (nonatomic, assign) CGRect currentItemFrame;
@property (nonatomic, assign) CGRect nextItemFrame;
@property (nonatomic, assign) BOOL infinite;
- (void)insertItemView:(UIView *)view atIndex:(NSInteger)index;
- (void)removeItemView:(UIView *)view;
@end
@implementation WXRecycleSliderView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
_currentIndex = 0;
_itemViews = [[NSMutableArray alloc] init];
_scrollView = [[WXRecycleSliderScrollView alloc] init];
_scrollView.backgroundColor = [UIColor clearColor];
_scrollView.delegate = self;
_scrollView.showsHorizontalScrollIndicator = NO;
_scrollView.showsVerticalScrollIndicator = NO;
_scrollView.scrollsToTop = NO;
[self addSubview:_scrollView];
}
return self;
}
- (void)dealloc
{
if (_scrollView) {
_scrollView.delegate = nil;
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self resetAllViewsFrame];
}
- (void)accessibilityDecrement
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.wx_component performSelector:NSSelectorFromString(@"resumeAutoPlay:") withObject:@(false)];
#pragma clang diagnostic pop
[self nextPage];
}
- (void)accessibilityIncrement
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.wx_component performSelector:NSSelectorFromString(@"resumeAutoPlay:") withObject:@(false)];
#pragma clang diagnostic pop
[self lastPage];
}
- (void)accessibilityElementDidLoseFocus
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.wx_component performSelector:NSSelectorFromString(@"resumeAutoPlay:") withObject:@(true)];
#pragma clang diagnostic pop
}
#pragma mark Private Methods
- (CGFloat)height {
return self.frame.size.height;
}
- (CGFloat)width {
return self.frame.size.width;
}
- (UIView *)getItemAtIndex:(NSInteger)index
{
if (self.itemViews.count > index) {
return [self.itemViews objectAtIndex:index];
}else{
return nil;
}
}
- (void)setCurrentIndex:(NSInteger)currentIndex
{
if (currentIndex >= _itemViews.count || currentIndex < 0) {
currentIndex = 0;
}
NSInteger oldIndex = _currentIndex;
_currentIndex = currentIndex;
if (_infinite) {
if (_direction == DirectionRight) {
self.nextItemFrame = CGRectMake(0, 0, self.width, self.height);
self.nextIndex = self.currentIndex - 1;
if (self.nextIndex < 0)
{
self.nextIndex = _itemViews.count - 1;
}
}else if (_direction == DirectionLeft) {
self.nextItemFrame = CGRectMake(self.width * 2, 0, self.width, self.height);
self.nextIndex = _itemViews.count?(self.currentIndex + 1) % _itemViews.count:0;
}else {
self.nextIndex = _itemViews.count?(self.currentIndex + 1) % _itemViews.count:0;
}
[self resetAllViewsFrame];
} else {
[_scrollView setContentOffset:CGPointMake(_currentIndex * self.width, 0) animated:YES];
}
[self resetIndicatorPoint];
if (self.delegate && [self.delegate respondsToSelector:@selector(recycleSliderView:didScrollToItemAtIndex:)]) {
if (oldIndex != _currentIndex) {
[self.delegate recycleSliderView:self didScrollToItemAtIndex:_currentIndex];
}
}
}
- (void)resetIndicatorPoint
{
[self.indicator setPointCount:self.itemViews.count];
[self.indicator setCurrentPoint:_currentIndex];
}
#pragma mark Scroll & Frames
- (void)setDirection:(Direction)direction {
if (_direction == direction) return;
_direction = direction;
if (_direction == DirectionNone) return;
if (_direction == DirectionRight) {
self.nextItemFrame = CGRectMake(0, 0, self.width, self.height);
self.nextIndex = self.currentIndex - 1;
if (self.nextIndex < 0)
{
self.nextIndex = _itemViews.count - 1;
}
UIView *view = [self getItemAtIndex:_nextIndex];
if (view) {
view.frame = _nextItemFrame;
}
}else if (_direction == DirectionLeft){
self.nextItemFrame = CGRectMake(self.width * 2, 0, self.width, self.height);
self.nextIndex = _itemViews.count?(self.currentIndex + 1) % _itemViews.count:0;
UIView *view = [self getItemAtIndex:_nextIndex];
if (view) {
view.frame = _nextItemFrame;
}
}
}
- (void)resetAllViewsFrame
{
if (_infinite && _itemViews.count > 1) {
self.scrollView.frame = CGRectMake(0, 0, self.width, self.height);
self.scrollView.contentOffset = CGPointMake(self.width, 0);
if (self.itemViews.count > 1) {
self.scrollView.contentSize = CGSizeMake(self.width * 3, 0);
} else {
self.scrollView.contentSize = CGSizeZero;
}
_currentItemFrame = CGRectMake(self.width, 0, self.width, self.height);
for (int i = 0; i < self.itemViews.count; i++) {
UIView *view = [self.itemViews objectAtIndex:i];
if (i != self.currentIndex) {
view.frame = CGRectMake(self.frame.size.width * 3, 0, self.width, self.height);;
}
}
[self getItemAtIndex:_currentIndex].frame = _currentItemFrame;
if (_itemViews.count == 2) {
_nextItemFrame = CGRectMake(self.width * 2, 0, self.width, self.height);
[self getItemAtIndex:_nextIndex].frame = _nextItemFrame;
}
} else {
self.scrollView.frame = self.bounds;
self.scrollView.contentSize = CGSizeMake(self.width * _itemViews.count, self.height);
self.scrollView.contentOffset = CGPointMake(_currentIndex * self.width, 0);
for (int i = 0; i < _itemViews.count; i ++) {
UIView *view = [_itemViews objectAtIndex:i];
view.frame = CGRectMake(i * self.width, 0, self.width, self.height);
}
[self.scrollView setContentOffset:CGPointMake(_currentIndex * self.width, 0) animated:NO];
}
[self resetIndicatorPoint];
}
- (void)nextPage {
if (_itemViews.count > 1) {
if (_infinite) {
[self.scrollView setContentOffset:CGPointMake(self.width * 2, 0) animated:YES];
} else {
// the currentindex will be set at the end of animation
NSInteger nextIndex = self.currentIndex + 1;
if(nextIndex < _itemViews.count) {
[self.scrollView setContentOffset:CGPointMake(nextIndex * self.width, 0) animated:YES];
}
}
}
}
- (void)lastPage
{
NSInteger lastIndex = [self currentIndex]-1;
if (_itemViews.count > 1) {
if (_infinite) {
if (lastIndex < 0) {
lastIndex = [_itemViews count]-1;
}
}
[self setCurrentIndex:lastIndex];
}
}
- (void)resetScrollView
{
if (WXFloatEqual(self.scrollView.contentOffset.x / self.width , 1.0))
{
return;
}
[self setCurrentIndex:self.nextIndex];
self.scrollView.contentOffset = CGPointMake(self.width, 0);
}
#pragma mark Public Methods
- (void)setIndicator:(WXIndicatorView *)indicator
{
_indicator = indicator;
[_indicator setPointCount:self.itemViews.count];
[_indicator setCurrentPoint:_currentIndex];
}
- (void)insertItemView:(UIView *)view atIndex:(NSInteger)index
{
if (![self.itemViews containsObject:view]) {
view.tag = self.itemViews.count;
if (index < 0) {
[self.itemViews addObject:view];
} else {
[self.itemViews insertObject:view atIndex:index];
}
}
if (![self.scrollView.subviews containsObject:view]) {
if (index < 0) {
[self.scrollView addSubview:view];
} else {
[self.scrollView insertSubview:view atIndex:index];
}
}
[self layoutSubviews];
[self setCurrentIndex:_currentIndex];
}
- (void)removeItemView:(UIView *)view
{
if ([self.itemViews containsObject:view]) {
[self.itemViews removeObject:view];
}
if ([self.scrollView.subviews containsObject:view]) {
[view removeFromSuperview];
}
[self layoutSubviews];
[self setCurrentIndex:_currentIndex];
}
#pragma mark ScrollView Delegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (_infinite) {
CGFloat offX = scrollView.contentOffset.x;
self.direction = offX > self.width ? DirectionLeft : offX < self.width ? DirectionRight : DirectionNone;
}
if (self.delegate && [self.delegate respondsToSelector:@selector(recycleSliderView:didScroll:)]) {
[self.delegate recycleSliderView:self didScroll:self.scrollView];
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
if (self.delegate && [self.delegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
[self.delegate scrollViewWillBeginDragging:self.scrollView];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (self.delegate && [self.delegate respondsToSelector:@selector(scrollViewDidEndDragging: willDecelerate:)]) {
[self.delegate scrollViewDidEndDragging:self.scrollView willDecelerate:decelerate];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (_infinite) {
[self resetScrollView];
} else {
NSInteger index = _scrollView.contentOffset.x / self.width;
[self setCurrentIndex:index];
}
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
if (_infinite) {
[self resetScrollView];
} else {
NSInteger index = _scrollView.contentOffset.x / self.width;
[self setCurrentIndex:index];
}
}
@end
@interface WXCycleSliderComponent () <WXRecycleSliderViewDelegate,WXIndicatorComponentDelegate>
@property (nonatomic, strong) WXRecycleSliderView *recycleSliderView;
@property (nonatomic, strong) NSTimer *autoTimer;
@property (nonatomic, assign) NSInteger currentIndex;
@property (nonatomic, assign) BOOL autoPlay;
@property (nonatomic, assign) NSUInteger interval;
@property (nonatomic, assign) NSInteger index;
@property (nonatomic, assign) CGFloat lastOffsetXRatio;
@property (nonatomic, assign) CGFloat offsetXAccuracy;
@property (nonatomic, assign) BOOL sliderChangeEvent;
@property (nonatomic, assign) BOOL sliderScrollEvent;
@property (nonatomic, assign) BOOL sliderScrollStartEvent;
@property (nonatomic, assign) BOOL sliderScrollEndEvent;
@property (nonatomic, assign) BOOL sliderStartEventFired;
@property (nonatomic, strong) NSMutableArray *childrenView;
@property (nonatomic, assign) BOOL scrollable;
@property (nonatomic, assign) BOOL infinite;
@end
@implementation WXCycleSliderComponent
- (void) dealloc
{
[self _stopAutoPlayTimer];
}
- (instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance
{
if (self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance]) {
_sliderChangeEvent = NO;
_sliderScrollEvent = NO;
_interval = 3000;
_childrenView = [NSMutableArray new];
_lastOffsetXRatio = 0;
if (attributes[@"autoPlay"]) {
_autoPlay = [WXConvert BOOL:attributes[@"autoPlay"]];
}
if (attributes[@"interval"]) {
_interval = [WXConvert NSInteger:attributes[@"interval"]];
}
if (attributes[@"index"]) {
_index = [WXConvert NSInteger:attributes[@"index"]];
}
_scrollable = attributes[@"scrollable"] ? [WXConvert BOOL:attributes[@"scrollable"]] : YES;
if (attributes[@"offsetXAccuracy"]) {
_offsetXAccuracy = [WXConvert CGFloat:attributes[@"offsetXAccuracy"]];
}
_infinite = attributes[@"infinite"] ? [WXConvert BOOL:attributes[@"infinite"]] : YES;
self.flexCssNode->setFlexDirection(WeexCore::kFlexDirectionRow,NO);
}
return self;
}
- (UIView *)loadView
{
return [[WXRecycleSliderView alloc] init];
}
- (void)viewDidLoad
{
[super viewDidLoad];
_recycleSliderView = (WXRecycleSliderView *)self.view;
_recycleSliderView.delegate = self;
_recycleSliderView.scrollView.pagingEnabled = YES;
_recycleSliderView.exclusiveTouch = YES;
_recycleSliderView.scrollView.scrollEnabled = _scrollable;
_recycleSliderView.infinite = _infinite;
UIAccessibilityTraits traits = UIAccessibilityTraitAdjustable;
if (_autoPlay) {
traits |= UIAccessibilityTraitUpdatesFrequently;
[self _startAutoPlayTimer];
} else {
[self _stopAutoPlayTimer];
}
_recycleSliderView.accessibilityTraits = traits;
}
- (void)layoutDidFinish
{
_recycleSliderView.currentIndex = _index;
}
- (void)viewDidUnload
{
[_childrenView removeAllObjects];
}
- (void)insertSubview:(WXComponent *)subcomponent atIndex:(NSInteger)index
{
if (subcomponent->_positionType == WXPositionTypeFixed) {
[self.weexInstance.rootView addSubview:subcomponent.view];
return;
}
// use _lazyCreateView to forbid component like cell's view creating
if(_lazyCreateView) {
subcomponent->_lazyCreateView = YES;
}
if (!subcomponent->_lazyCreateView || (self->_lazyCreateView && [self isViewLoaded])) {
UIView *view = subcomponent.view;
if(index < 0) {
[self.childrenView addObject:view];
}
else {
[self.childrenView insertObject:view atIndex:index];
}
WXRecycleSliderView *recycleSliderView = (WXRecycleSliderView *)self.view;
if ([view isKindOfClass:[WXIndicatorView class]]) {
((WXIndicatorComponent *)subcomponent).delegate = self;
[recycleSliderView addSubview:view];
[self setIndicatorView:(WXIndicatorView *)view];
return;
}
subcomponent.isViewFrameSyncWithCalculated = NO;
if (index == -1) {
[recycleSliderView insertItemView:view atIndex:index];
} else {
NSInteger offset = 0;
for (int i = 0; i < [self.childrenView count]; ++i) {
if (index == i) break;
if ([self.childrenView[i] isKindOfClass:[WXIndicatorView class]]) {
offset++;
}
}
[recycleSliderView insertItemView:view atIndex:index - offset];
// check if should apply current contentOffset
// in case inserting subviews after layoutDidFinish
if (index-offset == _index && _index>0) {
recycleSliderView.currentIndex = _index;
}
}
[recycleSliderView layoutSubviews];
}
}
- (void)willRemoveSubview:(WXComponent *)component
{
UIView *view = component.view;
if(self.childrenView && [self.childrenView containsObject:view]) {
[self.childrenView removeObject:view];
}
WXRecycleSliderView *recycleSliderView = (WXRecycleSliderView *)_view;
[recycleSliderView removeItemView:view];
if (self.childrenView.count <= _index) {
[recycleSliderView setCurrentIndex:0];
}
}
- (void)updateAttributes:(NSDictionary *)attributes
{
if (attributes[@"autoPlay"]) {
_autoPlay = [WXConvert BOOL:attributes[@"autoPlay"]];
if (_autoPlay) {
[self _startAutoPlayTimer];
} else {
[self _stopAutoPlayTimer];
}
}
if (attributes[@"interval"]) {
_interval = [WXConvert NSInteger:attributes[@"interval"]];
[self _stopAutoPlayTimer];
if (_autoPlay) {
[self _startAutoPlayTimer];
}
}
if (attributes[@"index"]) {
_index = [WXConvert NSInteger:attributes[@"index"]];
self.currentIndex = _index;
self.recycleSliderView.currentIndex = _index;
}
if (attributes[@"scrollable"]) {
_scrollable = attributes[@"scrollable"] ? [WXConvert BOOL:attributes[@"scrollable"]] : YES;
((WXRecycleSliderView *)self.view).scrollView.scrollEnabled = _scrollable;
}
if (attributes[@"offsetXAccuracy"]) {
_offsetXAccuracy = [WXConvert CGFloat:attributes[@"offsetXAccuracy"]];
}
if (attributes[@"infinite"]) {
_infinite = [WXConvert BOOL:attributes[@"infinite"]];
}
}
- (void)addEvent:(NSString *)eventName
{
if ([eventName isEqualToString:@"change"]) {
_sliderChangeEvent = YES;
}
if ([eventName isEqualToString:@"scroll"]) {
_sliderScrollEvent = YES;
}
}
- (void)removeEvent:(NSString *)eventName
{
if ([eventName isEqualToString:@"change"]) {
_sliderChangeEvent = NO;
}
if ([eventName isEqualToString:@"scroll"]) {
_sliderScrollEvent = NO;
}
}
- (BOOL)requestGestureShouldStopPropagation:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
return [self gestureShouldStopPropagation:gestureRecognizer shouldReceiveTouch:touch];
}
#pragma mark WXIndicatorComponentDelegate Methods
-(void)setIndicatorView:(WXIndicatorView *)indicatorView
{
NSAssert(_recycleSliderView, @"");
[_recycleSliderView setIndicator:indicatorView];
}
- (void)resumeAutoPlay:(id)resume
{
if (_autoPlay) {
if ([resume boolValue]) {
[self _startAutoPlayTimer];
} else {
[self _stopAutoPlayTimer];
}
}
}
#pragma mark Private Methods
- (void)_startAutoPlayTimer
{
if (!self.autoTimer || ![self.autoTimer isValid]) {
__weak __typeof__(self) weakSelf = self;
self.autoTimer = [NSTimer wx_scheduledTimerWithTimeInterval:_interval/1000.0f block:^() {
[weakSelf _autoPlayOnTimer];
} repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.autoTimer forMode:NSRunLoopCommonModes];
}
}
- (void)_stopAutoPlayTimer
{
if (self.autoTimer && [self.autoTimer isValid]) {
[self.autoTimer invalidate];
self.autoTimer = nil;
}
}
- (void)_autoPlayOnTimer
{
if (!_infinite && (_currentIndex == _recycleSliderView.itemViews.count - 1)) {
[self _stopAutoPlayTimer];
}else {
[self.recycleSliderView nextPage];
}
}
#pragma mark ScrollView Delegate
- (void)recycleSliderView:(WXRecycleSliderView *)recycleSliderView didScroll:(UIScrollView *)scrollView
{
if (_sliderScrollEvent) {
CGFloat width = scrollView.frame.size.width;
CGFloat XDeviation = 0;
if (_infinite) {
XDeviation = - (scrollView.contentOffset.x - width);
} else {
XDeviation = - (scrollView.contentOffset.x - width * _currentIndex);
}
CGFloat offsetXRatio = (XDeviation / width);
if (fabs(offsetXRatio - _lastOffsetXRatio) >= _offsetXAccuracy) {
_lastOffsetXRatio = offsetXRatio;
[self fireEvent:@"scroll" params:@{@"offsetXRatio":[NSNumber numberWithFloat:offsetXRatio]} domChanges:nil];
}
}
}
- (void)recycleSliderView:(WXRecycleSliderView *)recycleSliderView didScrollToItemAtIndex:(NSInteger)index
{
if (_sliderChangeEvent) {
[self fireEvent:@"change" params:@{@"index":@(index)} domChanges:@{@"attrs": @{@"index": @(index)}}];
}
self.currentIndex = index;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[self _stopAutoPlayTimer];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (_autoPlay) {
[self _startAutoPlayTimer];
}
}
@end
@implementation WXRecycleSliderScrollView
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
WXRecycleSliderView *view = (WXRecycleSliderView *)self.delegate;
if (![view isKindOfClass:[UIView class]]) {
return YES;
}
if ([(id <WXRecycleSliderViewDelegate>) view.wx_component respondsToSelector:@selector(requestGestureShouldStopPropagation:shouldReceiveTouch:)]) {
return [(id <WXRecycleSliderViewDelegate>) view.wx_component requestGestureShouldStopPropagation:gestureRecognizer shouldReceiveTouch:touch];
}
else{
return YES;
}
}
@end