blob: 94fbc6734d2fca80cc438c3b982d2d08f025ed18 [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 "WXSliderNeighborComponent.h"
#import "WXConvert.h"
#import "WXUtility.h"
#import "WXComponent_internal.h"
#import "WXIndicatorComponent.h"
#import "WXSDKInstance.h"
#import "NSTimer+Weex.h"
#import "WXComponent+Layout.h"
#define MAX_VISIBLE_ITEMS 30
#define MIN_TOGGLE_DURATION 0.2
#define MAX_TOGGLE_DURATION 0.4
#define SCROLL_DURATION 0.4
#define INSERT_DURATION 0.4
#define DECELERATE_THRESHOLD 0.1
#define SCROLL_SPEED_THRESHOLD 2.0
#define SCROLL_DISTANCE_THRESHOLD 0.1
#define DECELERATION_MULTIPLIER 30.0
#define FLOAT_ERROR_MARGIN 0.000001
@protocol WXSliderNeighborDelegate, WXSliderNeighborDataSource;
@interface WXSliderNeighborView: UIView
@property (nonatomic, strong) WXIndicatorView *indicator;
@property (nonatomic, weak) id<WXSliderNeighborDelegate> delegate;
@property (nonatomic, weak) id<WXSliderNeighborDataSource> dataSource;
@property (nonatomic, assign) CGFloat perspective;
@property (nonatomic, readonly, getter = isDragging) BOOL dragging;
@property (nonatomic, readonly, getter = isScrolling) BOOL scrolling;
@property (nonatomic, assign) CGFloat autoscroll;
@property (nonatomic, assign) CGFloat scrollOffset;
@property (nonatomic, assign) CGFloat previousTranslation;
@property (nonatomic, assign, getter = isVertical) BOOL vertical;
@property (nonatomic, assign) CGFloat decelerationRate;
@property (nonatomic, assign, getter = isScrollEnabled) BOOL scrollEnabled;
@property (nonatomic, assign, getter = isPagingEnabled) BOOL pagingEnabled;
@property (nonatomic, assign) BOOL bounces;
@property (nonatomic, assign) BOOL didDrag;
@property (nonatomic, readonly, getter = isDecelerating) BOOL decelerating;
@property (nonatomic, strong, readonly) UIView *contentView;
@property (nonatomic, strong) NSMutableDictionary *itemViews;
@property (nonatomic, readonly) NSInteger numberOfVisibleItems;
@property (nonatomic, readonly) NSInteger numberOfItems;
@property (nonatomic, readonly) NSInteger numberOfPlaceholders;
@property (nonatomic, strong) NSMutableSet *itemViewPool;
@property (nonatomic, strong) NSMutableSet *placeholderViewPool;
@property (nonatomic, assign) NSTimeInterval startTime;
@property (nonatomic, assign) NSTimeInterval lastTime;
@property (nonatomic, assign) CGFloat startVelocity;
@property (nonatomic, assign) CGFloat offsetMultiplier;
@property (nonatomic, assign) CGFloat startOffset;
@property (nonatomic, assign) CGFloat endOffset;
@property (nonatomic, assign) CGSize contentOffset;
@property (nonatomic, assign) CGSize viewpointOffset;
@property (nonatomic, assign) CGFloat scrollSpeed;
@property (nonatomic, assign) CGFloat bounceDistance;
@property (nonatomic, assign) BOOL stopAtItemBoundary;
@property (nonatomic, assign) BOOL scrollToItemBoundary;
@property (nonatomic, assign) BOOL centerItemWhenSelected;
@property (nonatomic, assign) NSTimeInterval scrollDuration;
@property (nonatomic, readonly, getter = isWrapEnabled) BOOL wrapEnabled;
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) NSInteger numberOfPlaceholdersToShow;
@property (nonatomic, assign) CGFloat previousScrollOffset;
@property (nonatomic, assign) NSTimeInterval toggleTime;
@property (nonatomic, readonly) CGFloat toggle;
@property (nonatomic, assign) NSInteger previousItemIndex;
@property (nonatomic, readonly) CGFloat itemWidth;
@property (nonatomic, assign) BOOL inited;
@end
@implementation WXSliderNeighborView
- (instancetype)init {
self = [super init];
_decelerationRate = 0.01;
_scrollEnabled = YES;
_bounces = YES;
_offsetMultiplier = 1.0;
_perspective = -1.0/500.0;
_contentOffset = CGSizeZero;
_viewpointOffset = CGSizeZero;
_scrollSpeed = 1.0;
_bounceDistance = 1.0;
_stopAtItemBoundary = YES;
_scrollToItemBoundary = YES;
_centerItemWhenSelected = YES;
_inited = NO;
_contentView = [[UIView alloc] initWithFrame:self.bounds];
//pan gesture recognizer
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didPan:)];
panGesture.delegate = (id <UIGestureRecognizerDelegate>)self;
[_contentView addGestureRecognizer:panGesture];
//tap gesture recognizer
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTap:)];
tapGesture.delegate = (id <UIGestureRecognizerDelegate>)self;
[_contentView addGestureRecognizer:tapGesture];
self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction;
self.isAccessibilityElement = YES;
[self addSubview:_contentView];
if (_dataSource) {
[self reloadData];
}
return self;
}
- (void)dealloc
{
[self stopAnimation];
}
- (void)accessibilityIncrement
{
[self setCurrentItemIndex: [self lastItemIndex]];
}
- (void)accessibilityDecrement
{
[self setCurrentItemIndex:[self nextItemIndex]];
}
- (void)setDataSource:(id<WXSliderNeighborDataSource>)dataSource
{
if (_dataSource != dataSource) {
_dataSource = dataSource;
if (_dataSource) {
[self reloadData];
}
}
}
- (void)setDelegate:(id<WXSliderNeighborDelegate>)delegate
{
if (_delegate != delegate) {
_delegate = delegate;
if (_delegate && _dataSource) {
[self setNeedsLayout];
}
}
}
- (void)setVertical:(BOOL)vertical
{
if (_vertical != vertical)
{
_vertical = vertical;
[self layOutItemViews];
}
}
#pragma clang diagnostic push
#pragma GCC diagnostic ignored "-Wundeclared-selector"
- (void)setCurrentItemIndex:(NSInteger)currentItemIndex
{
if ([self currentItemIndex] == currentItemIndex) return;
[self setScrollOffset:currentItemIndex];
[self.indicator setCurrentPoint:currentItemIndex];
if (self.delegate && [self.delegate respondsToSelector:@selector(sliderView:didScrollToItemAtIndex:)]) {
[self.delegate sliderNeighbor:self didScrollToItemAtIndex:currentItemIndex];
}
[self scroll2ItemViewAtIndex:currentItemIndex animated:YES];
}
#pragma clang diagnostic pop
- (void)updateItemWidth
{
_itemWidth = [_delegate sliderNeighborItemWidth:self] ?: _itemWidth;
if (_numberOfItems > 0) {
if ([_itemViews count] == 0) {
[self loadViewAtIndex:0];
}
} else if (_numberOfPlaceholders > 0) {
if ([_itemViews count] == 0) {
[self loadViewAtIndex:-1];
}
}
}
- (NSInteger)circularCarouselItemCount
{
NSInteger count = 0;
CGFloat spacing = [self valueForOption:WXSliderNeighborOptionSpacing withDefault:1.0];
CGFloat width = _vertical ? self.bounds.size.height: self.bounds.size.width;
count = MIN(MAX_VISIBLE_ITEMS, MAX(12, ceil(width / (spacing * _itemWidth)) * M_PI));
count = MIN(_numberOfItems + _numberOfPlaceholdersToShow, count);
return [self valueForOption:WXSliderNeighborOptionCount withDefault:count];
}
- (void)updateNumberOfVisibleItems
{
//get number of visible items
//based on count value
CGFloat spacing = [self valueForOption:WXSliderNeighborOptionSpacing withDefault:1.0];
CGFloat width = _vertical ? self.bounds.size.height: self.bounds.size.width;
CGFloat itemWidth = _itemWidth * spacing;
_numberOfVisibleItems = ceil(width / itemWidth) + 2;
_numberOfVisibleItems = MIN(MAX_VISIBLE_ITEMS, _numberOfVisibleItems);
_numberOfVisibleItems = [self valueForOption:WXSliderNeighborOptionVisibleItems withDefault:_numberOfVisibleItems];
_numberOfVisibleItems = MAX(0, MIN(_numberOfVisibleItems, _numberOfItems + _numberOfPlaceholdersToShow));
}
- (CGFloat)offsetForItemAtIndex:(NSInteger)index
{
//calculate relative position
CGFloat offset = index - _scrollOffset;
if (_wrapEnabled) {
if (offset > _numberOfItems/2.0) {
offset -= _numberOfItems;
} else if (offset < -_numberOfItems/2.0) {
offset += _numberOfItems;
}
}
return offset;
}
- (CGFloat)alphaForItemWithOffset:(CGFloat)offset
{
CGFloat fadeMin = -INFINITY;
CGFloat fadeMax = INFINITY;
CGFloat fadeRange = 1.0;
CGFloat fadeMinAlpha = 0.0;
fadeMin = [self valueForOption:WXSliderNeighborOptionFadeMin withDefault:fadeMin];
fadeMax = [self valueForOption:WXSliderNeighborOptionFadeMax withDefault:fadeMax];
fadeRange = [self valueForOption:WXSliderNeighborOptionFadeRange withDefault:fadeRange];
fadeMinAlpha = [self valueForOption:WXSliderNeighborOptionFadeMinAlpha withDefault:fadeMinAlpha];
CGFloat factor = 0.0;
if (offset > fadeMax) {
factor = offset - fadeMax;
} else if (offset < fadeMin) {
factor = fadeMin - offset;
}
return 1.0 - MIN(factor, fadeRange) / fadeRange * (1.0 - fadeMinAlpha);
}
- (UIView *)containView:(UIView *)view
{
//set item width
if (!_itemWidth) {
_itemWidth = _vertical? view.frame.size.height: view.frame.size.width;
}
//set container frame
CGRect frame = view.frame;
frame.size.width = _vertical? frame.size.width: _itemWidth;
frame.size.height = _vertical? _itemWidth: frame.size.height;
UIView *containerView = [[UIView alloc] initWithFrame:frame];
//set view frame
frame = view.frame;
frame.origin.x = (containerView.bounds.size.width - frame.size.width) / 2.0;
frame.origin.y = (containerView.bounds.size.height - frame.size.height) / 2.0;
view.frame = frame;
[containerView addSubview:view];
containerView.layer.opacity = 0;
containerView.transform = CGAffineTransformMakeScale(0.8, 1.0);
return containerView;
}
- (void)transformItemViews
{
for (NSNumber *number in _itemViews)
{
NSInteger index = [number integerValue];
UIView *view = _itemViews[number];
[self transformItemView:view atIndex:index];
}
}
- (CATransform3D)transformForItemViewWithOffset:(CGFloat)offset
{
//set up base transform
CATransform3D transform = CATransform3DIdentity;
transform.m34 = _perspective;
transform = CATransform3DTranslate(transform, -_viewpointOffset.width, -_viewpointOffset.height, 0.0);
//perform transforms
CGFloat spacing = [self valueForOption:WXSliderNeighborOptionSpacing withDefault:1.0];
if (_vertical) {
return CATransform3DTranslate(transform, 0.0, offset * _itemWidth * spacing, 0.0);
} else {
return CATransform3DTranslate(transform, offset * _itemWidth * spacing, 0.0, 0.0);
}
}
- (void)transformItemView:(UIView *)view atIndex:(NSInteger)index
{
//calculate offset
CGFloat offset = [self offsetForItemAtIndex:index];
//update alpha
view.superview.layer.opacity = [self alphaForItemWithOffset:offset];
//center view
view.superview.center = CGPointMake(self.bounds.size.width/2.0 + _contentOffset.width,
self.bounds.size.height/2.0 + _contentOffset.height);
//enable/disable interaction
view.superview.userInteractionEnabled = (!_centerItemWhenSelected || index == self.currentItemIndex);
view.superview.layer.rasterizationScale = [UIScreen mainScreen].scale;
[view layoutIfNeeded];
CGFloat clampedOffset = MAX(-1.0, MIN(1.0, offset));
if (_decelerating || (_scrolling && !_dragging && !_didDrag) || (_autoscroll && !_dragging) ||
(!_wrapEnabled && (_scrollOffset < 0 || _scrollOffset >= _numberOfItems - 1))) {
if (offset > 0) {
_toggle = (offset <= 0.5)? -clampedOffset: (1.0 - clampedOffset);
} else {
_toggle = (offset > -0.5)? -clampedOffset: (- 1.0 - clampedOffset);
}
}
//calculate transform
CATransform3D transform = [self transformForItemViewWithOffset:offset];
//transform view
view.superview.layer.transform = transform;
//backface culling
BOOL showBackfaces = view.layer.doubleSided;
if (showBackfaces) {
showBackfaces = YES;
}
showBackfaces = !![self valueForOption:WXSliderNeighborOptionShowBackfaces withDefault:showBackfaces];
view.superview.hidden = !(showBackfaces ?: (transform.m33 > 0.0));
}
NSComparisonResult sliderNeighorCompareViewDepth(UIView *view1, UIView *view2, WXSliderNeighborView *self)
{
//compare depths
CATransform3D t1 = view1.superview.layer.transform;
CATransform3D t2 = view2.superview.layer.transform;
CGFloat z1 = t1.m13 + t1.m23 + t1.m33 + t1.m43;
CGFloat z2 = t2.m13 + t2.m23 + t2.m33 + t2.m43;
CGFloat difference = z1 - z2;
//if depths are equal, compare distance from current view
if (difference == 0.0) {
CATransform3D t3 = [self currentItemView].superview.layer.transform;
if (self.vertical) {
CGFloat y1 = t1.m12 + t1.m22 + t1.m32 + t1.m42;
CGFloat y2 = t2.m12 + t2.m22 + t2.m32 + t2.m42;
CGFloat y3 = t3.m12 + t3.m22 + t3.m32 + t3.m42;
difference = fabs(y2 - y3) - fabs(y1 - y3);
} else {
CGFloat x1 = t1.m11 + t1.m21 + t1.m31 + t1.m41;
CGFloat x2 = t2.m11 + t2.m21 + t2.m31 + t2.m41;
CGFloat x3 = t3.m11 + t3.m21 + t3.m31 + t3.m41;
difference = fabs(x2 - x3) - fabs(x1 - x3);
}
}
return (difference < 0.0)? NSOrderedAscending: NSOrderedDescending;
}
- (void)depthSortViews
{
for (UIView *view in [[_itemViews allValues] sortedArrayUsingFunction:(NSInteger (*)(id, id, void *))sliderNeighorCompareViewDepth context:(__bridge void *)self])
{
[_contentView bringSubviewToFront:(UIView *__nonnull)view.superview];
}
}
- (void)layOutItemViews
{
//bail out if not set up yet
if (!_dataSource || !_contentView) {
return;
}
//update wrap
_wrapEnabled = YES;
_wrapEnabled = !![self valueForOption:WXSliderNeighborOptionWrap withDefault:_wrapEnabled];
//no placeholders on wrapped carousels
_numberOfPlaceholdersToShow = _wrapEnabled? 0: _numberOfPlaceholders;
//set item width
[self updateItemWidth];
//update number of visible items
[self updateNumberOfVisibleItems];
//prevent false index changed event
_previousScrollOffset = self.scrollOffset;
//update offset multiplier
_offsetMultiplier = 1.0;
_offsetMultiplier = [self valueForOption:WXSliderNeighborOptionOffsetMultiplier withDefault:_offsetMultiplier];
//align
if (!_scrolling && !_decelerating && !_autoscroll) {
if (_scrollToItemBoundary && self.currentItemIndex != -1) {
[self scroll2ItemViewAtIndex:self.currentItemIndex animated:YES];
} else {
_scrollOffset = [self clampedOffset:_scrollOffset];
}
}
//update views
if (_inited) {
[self didScroll];
}
_inited = YES;
}
- (CGFloat)valueForOption:(WXSliderNeighborOption)option withDefault:(CGFloat)value
{
if (_delegate && [_delegate respondsToSelector:@selector(sliderNeighbor:valueForOption:withDefault:)]) {
return [_delegate sliderNeighbor:self valueForOption:option withDefault:value];
}
return value;
}
#pragma mark -
#pragma mark Gestures and taps
- (NSInteger)viewOrSuperviewIndex:(UIView *)view
{
if (view == nil || view == _contentView) {
return NSNotFound;
}
NSInteger index = [self indexOfItemView:view];
if (index == NSNotFound) {
return [self viewOrSuperviewIndex:view.superview];
}
return index;
}
- (BOOL)viewOrSuperview:(UIView *)view implementsSelector:(SEL)selector
{
if (!view || view == self.contentView) {
return NO;
}
Class viewClass = [view class];
while (viewClass && viewClass != [UIView class])
{
unsigned int numberOfMethods;
Method *methods = class_copyMethodList(viewClass, &numberOfMethods);
for (unsigned int i = 0; i < numberOfMethods; i++)
{
if (method_getName(methods[i]) == selector) {
free(methods);
return YES;
}
}
if (methods) free(methods);
viewClass = [viewClass superclass];
}
return [self viewOrSuperview:view.superview implementsSelector:selector];
}
- (id)viewOrSuperview:(UIView *)view ofClass:(Class)swizzClass
{
if (!view || view == self.contentView) {
return nil;
} else if ([view isKindOfClass:swizzClass]) {
return view;
}
return [self viewOrSuperview:view.superview ofClass:swizzClass];
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gesture shouldReceiveTouch:(UITouch *)touch
{
if (_scrollEnabled) {
_dragging = NO;
_scrolling = NO;
_decelerating = NO;
}
if ([gesture isKindOfClass:[UITapGestureRecognizer class]]) {
//handle tap
NSInteger index = [self viewOrSuperviewIndex:touch.view];
if (index == NSNotFound && _centerItemWhenSelected) {
//view is a container view
index = [self viewOrSuperviewIndex:[touch.view.subviews lastObject]];
}
if (index != NSNotFound) {
if ([self viewOrSuperview:touch.view implementsSelector:@selector(touchesBegan:withEvent:)]) {
return NO;
}
}
} else if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
if (!_scrollEnabled) {
return NO;
} else if ([self viewOrSuperview:touch.view implementsSelector:@selector(touchesMoved:withEvent:)]) {
UIScrollView *scrollView = [self viewOrSuperview:touch.view ofClass:[UIScrollView class]];
if (scrollView) {
return !scrollView.scrollEnabled ||
(self.vertical && scrollView.contentSize.height <= scrollView.frame.size.height) ||
(!self.vertical && scrollView.contentSize.width <= scrollView.frame.size.width);
}
if ([self viewOrSuperview:touch.view ofClass:[UIButton class]] ||
[self viewOrSuperview:touch.view ofClass:[UIBarButtonItem class]]) {
return YES;
}
return NO;
}
}
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
//if the view which the otherGestureRecognizer works on is a scrollview and also it is scrollEnabled vertically ,at this time,we should not block the guesture from being recognized by the otherGestureRecognize
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
if ([otherGestureRecognizer.view isKindOfClass:[UIScrollView class]]) {
UIScrollView* scrollview = (UIScrollView *)otherGestureRecognizer.view;
if (scrollview.scrollEnabled) {
UIPanGestureRecognizer* panRcgn= (UIPanGestureRecognizer *)gestureRecognizer;
//check offset for confirming vertival movement
if (fabs([panRcgn translationInView:panRcgn.view].y) > fabs([panRcgn translationInView:panRcgn.view].x)*16) {
return YES;
}
}
}
}
return NO;
}
- (void)didPan:(UIPanGestureRecognizer *)panGesture
{
if (_scrollEnabled && _numberOfItems) {
switch (panGesture.state)
{
case UIGestureRecognizerStateBegan:
{
_dragging = YES;
_scrolling = NO;
_decelerating = NO;
_previousTranslation = _vertical? [panGesture translationInView:self].y: [panGesture translationInView:self].x;
[_delegate sliderNeighborWillBeginDragging:self];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateFailed:
{
_dragging = NO;
_didDrag = YES;
if ([self shouldDecelerate]) {
_didDrag = NO;
[self startDecelerating];
}
[self pushAnimationState:YES];
[_delegate sliderNeighborDidEndDragging:self willDecelerate:_decelerating];
[self popAnimationState];
if (!_decelerating) {
if ((_scrollToItemBoundary || fabs(_scrollOffset - [self clampedOffset:_scrollOffset]) > FLOAT_ERROR_MARGIN) && !_autoscroll) {
if (fabs(_scrollOffset - self.currentItemIndex) < FLOAT_ERROR_MARGIN) {
//call scroll to trigger events for legacy support reasons
//even though technically we don't need to scroll at all
[self scroll2ItemViewAtIndex:self.currentItemIndex duration:0.01];
} else {
[self scroll2ItemViewAtIndex:self.currentItemIndex animated:YES];
}
} else {
[self depthSortViews];
}
} else {
[self pushAnimationState:YES];
[_delegate sliderNeighborWillBeginDecelerating:self];
[self popAnimationState];
}
break;
}
case UIGestureRecognizerStateChanged:
{
CGFloat translation = _vertical? [panGesture translationInView:self].y: [panGesture translationInView:self].x;
CGFloat velocity = _vertical? [panGesture velocityInView:self].y: [panGesture velocityInView:self].x;
CGFloat factor = 1.0;
if (!_wrapEnabled && _bounces) {
factor = 1.0 - MIN(fabs(_scrollOffset - [self clampedOffset:_scrollOffset]),
_bounceDistance) / _bounceDistance;
}
_startVelocity = -velocity * factor * _scrollSpeed / _itemWidth;
_scrollOffset -= (translation - _previousTranslation) * factor * _offsetMultiplier / _itemWidth;
_previousTranslation = translation;
[self didScroll];
break;
}
case UIGestureRecognizerStatePossible:
{
//do nothing
break;
}
}
}
}
- (void)didTap:(UITapGestureRecognizer *)tapGesture
{
//check for tapped view
NSInteger index = [self indexOfItemView:[self itemViewAtPoint:[tapGesture locationInView:_contentView]]];
if (index != NSNotFound) {
if (!_delegate || [_delegate sliderNeighbor:self shouldSelectItemAtIndex:index]) {
if ((index != self.currentItemIndex && _centerItemWhenSelected) ||
(index == self.currentItemIndex && _scrollToItemBoundary)) {
[self scroll2ItemViewAtIndex:index animated:YES];
}
[_delegate sliderNeighbor:self didSelectItemAtIndex:index];
} else if (_scrollEnabled && _scrollToItemBoundary && _autoscroll) {
[self scroll2ItemViewAtIndex:self.currentItemIndex animated:YES];
}
} else {
[self scroll2ItemViewAtIndex:self.currentItemIndex animated:YES];
}
}
- (void)pushAnimationState:(BOOL)enabled
{
[CATransaction begin];
[CATransaction setDisableActions:!enabled];
}
- (void)popAnimationState
{
[CATransaction commit];
}
- (void)reloadData
{
//remove old views
for (UIView *view in [_itemViews allValues])
{
[view.superview removeFromSuperview];
}
//bail out if not set up yet
if (!_dataSource || !_contentView)
{
return;
}
//get number of items and placeholders
_numberOfVisibleItems = 0;
_numberOfItems = [_dataSource numberOfItemsInSliderNeighbor:self];
if ([_dataSource respondsToSelector:@selector(numberOfPlaceholdersInsliderNeighbor:)]) {
_numberOfPlaceholders = [_dataSource numberOfPlaceholdersInsliderNeighbor:self];
}
//reset view pools
self.itemViews = [NSMutableDictionary dictionary];
self.itemViewPool = [NSMutableSet set];
self.placeholderViewPool = [NSMutableSet setWithCapacity:_numberOfPlaceholders];
//layout views
[self setNeedsLayout];
//fix scroll offset
if (_numberOfItems > 0 && _scrollOffset < 0.0)
{
[self scroll2ItemViewAtIndex:0 animated:(_numberOfPlaceholders > 0)];
}
}
#pragma mark scrolling
- (NSInteger)currentItemIndex
{
return [self clampedIndex:round(_scrollOffset)];
}
- (NSInteger)nextItemIndex
{
return ([self currentItemIndex]+1)%_numberOfItems;
}
- (NSInteger)lastItemIndex
{
if ([self currentItemIndex] == 0) {
return _numberOfItems - 1;
}
return ((NSInteger)round(abs((int)[self currentItemIndex] - 1))) % _numberOfItems;
}
- (NSInteger)minScrollDistanceFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex
{
NSInteger directDistance = toIndex - fromIndex;
if (_wrapEnabled)
{
NSInteger wrappedDistance = MIN(toIndex, fromIndex) + _numberOfItems - MAX(toIndex, fromIndex);
if (fromIndex < toIndex)
{
wrappedDistance = -wrappedDistance;
}
return (ABS(directDistance) <= ABS(wrappedDistance))? directDistance: wrappedDistance;
}
return directDistance;
}
- (NSInteger)clampedIndex:(NSInteger)index
{
if (_numberOfItems == 0)
{
return -1;
} else if (_wrapEnabled) {
return index - floor((CGFloat)index / (CGFloat)_numberOfItems) * _numberOfItems;
} else {
return MIN(MAX(0, index), MAX(0, _numberOfItems - 1));
}
}
- (CGFloat)clampedOffset:(CGFloat)offset
{
if (_numberOfItems == 0) {
return -1.0;
} else if (_wrapEnabled) {
return offset - floor(offset / (CGFloat)_numberOfItems) * _numberOfItems;
} else {
return MIN(MAX(0.0, offset), MAX(0.0, (CGFloat)_numberOfItems - 1.0));
}
}
- (CGFloat)minScrollDistanceFromOffset:(CGFloat)fromOffset toOffset:(CGFloat)toOffset
{
CGFloat directDistance = toOffset - fromOffset;
if (_wrapEnabled) {
CGFloat wrappedDistance = MIN(toOffset, fromOffset) + _numberOfItems - MAX(toOffset, fromOffset);
if (fromOffset < toOffset) {
wrappedDistance = -wrappedDistance;
}
return (fabs(directDistance) <= fabs(wrappedDistance))? directDistance: wrappedDistance;
}
return directDistance;
}
- (void)scrollByOffset:(CGFloat)offset duration:(NSTimeInterval)duration
{
if (duration > 0.0) {
_decelerating = NO;
_scrolling = YES;
_startTime = CACurrentMediaTime();
_startOffset = _scrollOffset;
_scrollDuration = duration;
_endOffset = _startOffset + offset;
if (!_wrapEnabled) {
_endOffset = [self clampedOffset:_endOffset];
}
[_delegate sliderNeighborWillBeginScrollingAnimation:self];
[self startAnimation];
} else {
self.scrollOffset += offset;
}
}
- (void)scroll2Offset:(CGFloat)offset duration:(NSTimeInterval)duration
{
[self scrollByOffset:[self minScrollDistanceFromOffset:_scrollOffset toOffset:offset] duration:duration];
if (_delegate && [_delegate respondsToSelector:@selector(sliderNeighbor:didScrollToItemAtIndex:)]) {
[_delegate sliderNeighbor:self didScrollToItemAtIndex:offset];
}
}
- (void)scroll2ItemViewAtIndex:(NSInteger)index duration:(NSTimeInterval)duration
{
[self scroll2Offset:index duration:duration];
}
- (void)scroll2ItemViewAtIndex:(NSInteger)index animated:(BOOL)animated
{
[self scroll2ItemViewAtIndex:index duration:animated? 0.6: 0];
}
- (void)scrollByNumberOfItems:(NSInteger)itemCount duration:(NSTimeInterval)duration
{
if (duration > 0.0) {
CGFloat offset = 0.0;
if (itemCount > 0) {
offset = (floor(_scrollOffset) + itemCount) - _scrollOffset;
} else if (itemCount < 0) {
offset = (ceil(_scrollOffset) + itemCount) - _scrollOffset;
} else {
offset = round(_scrollOffset) - _scrollOffset;
}
[self scrollByOffset:offset duration:duration];
} else {
self.scrollOffset = [self clampedIndex:_previousItemIndex + itemCount];
}
}
- (void)removeItemAtIndex:(NSInteger)index animated:(BOOL)animated
{
index = [self clampedIndex:index];
UIView *itemView = [self itemViewAtIndex:index];
if (animated) {
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.1];
[UIView setAnimationDelegate:itemView.superview];
[UIView setAnimationDidStopSelector:@selector(removeFromSuperview)];
[self performSelector:@selector(queueItemView:) withObject:itemView afterDelay:0.1];
itemView.superview.layer.opacity = 0.0;
[UIView commitAnimations];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDelay:0.1];
[UIView setAnimationDuration:INSERT_DURATION];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(depthSortViews)];
[self removeViewAtIndex:index];
_numberOfItems --;
_wrapEnabled = !![self valueForOption:WXSliderNeighborOptionWrap withDefault:_wrapEnabled];
[self updateNumberOfVisibleItems];
_scrollOffset = self.currentItemIndex;
[self didScroll];
[UIView commitAnimations];
} else {
[self pushAnimationState:NO];
[self queueItemView:itemView];
[itemView.superview removeFromSuperview];
[self removeViewAtIndex:index];
_numberOfItems --;
_wrapEnabled = !![self valueForOption:WXSliderNeighborOptionWrap withDefault:_wrapEnabled];
_scrollOffset = self.currentItemIndex;
[self didScroll];
[self depthSortViews];
[self popAnimationState];
}
}
- (void)insertItemAtIndex:(NSInteger)index animated:(BOOL)animated
{
_numberOfItems ++;
_wrapEnabled = !![self valueForOption:WXSliderNeighborOptionWrap withDefault:_wrapEnabled];
[self updateNumberOfVisibleItems];
index = [self clampedIndex:index];
[self insertView:nil atIndex:index];
[self loadViewAtIndex:index];
if (fabs(_itemWidth) < FLOAT_ERROR_MARGIN) {
[self updateItemWidth];
}
if (animated) {
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:INSERT_DURATION];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(didScroll)];
[self transformItemViews];
[UIView commitAnimations];
} else {
[self pushAnimationState:NO];
[self didScroll];
[self popAnimationState];
}
if (_scrollOffset < 0.0) {
[self scroll2ItemViewAtIndex:0 animated:(animated && _numberOfPlaceholders)];
}
}
- (void)reloadItemAtIndex:(NSInteger)index animated:(BOOL)animated
{
//get container view
UIView *containerView = [[self itemViewAtIndex:index] superview];
if (containerView) {
if (animated) {
//fade transition
CATransition *transition = [CATransition animation];
transition.duration = INSERT_DURATION;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionFade;
[containerView.layer addAnimation:transition forKey:nil];
}
//reload view
[self loadViewAtIndex:index withContainerView:containerView];
}
}
#pragma mark Animation
- (void)step
{
[self pushAnimationState:NO];
NSTimeInterval currentTime = CACurrentMediaTime();
double delta = currentTime - _lastTime;
_lastTime = currentTime;
if (_scrolling && !_dragging) {
NSTimeInterval time = MIN(1.0, (currentTime - _startTime) / _scrollDuration);
delta = [self easeInOut:time];
_scrollOffset = _startOffset + (_endOffset - _startOffset) * delta;
[self didScroll];
if (time >= 1.0) {
_scrolling = NO;
[self depthSortViews];
[self pushAnimationState:YES];
[_delegate sliderNeighborDidEndScrollingAnimation:self];
[self popAnimationState];
}
} else if (_decelerating) {
CGFloat time = MIN(_scrollDuration, currentTime - _startTime);
CGFloat acceleration = -_startVelocity/_scrollDuration;
CGFloat distance = _startVelocity * time + 0.5 * acceleration * pow(time, 2.0);
_scrollOffset = _startOffset + distance;
[self didScroll];
if (fabs(time - _scrollDuration) < FLOAT_ERROR_MARGIN) {
_decelerating = NO;
[self pushAnimationState:YES];
[_delegate sliderNeighborDidEndDecelerating:self];
[self popAnimationState];
if ((_scrollToItemBoundary || fabs(_scrollOffset - [self clampedOffset:_scrollOffset]) > FLOAT_ERROR_MARGIN) && !_autoscroll) {
if (fabs(_scrollOffset - self.currentItemIndex) < FLOAT_ERROR_MARGIN) {
//call scroll to trigger events for legacy support reasons
//even though technically we don't need to scroll at all
[self scroll2ItemViewAtIndex:self.currentItemIndex duration:0.01];
} else {
[self scroll2ItemViewAtIndex:self.currentItemIndex animated:YES];
}
} else {
CGFloat difference = round(_scrollOffset) - _scrollOffset;
if (difference > 0.5) {
difference = difference - 1.0;
} else if (difference < -0.5) {
difference = 1.0 + difference;
}
_toggleTime = currentTime - MAX_TOGGLE_DURATION * fabs(difference);
_toggle = MAX(-1.0, MIN(1.0, -difference));
}
}
} else if (_autoscroll && !_dragging) {
//autoscroll goes backwards from what you'd expect, for historical reasons
self.scrollOffset = [self clampedOffset:_scrollOffset - delta * _autoscroll];
} else if (fabs(_toggle) > FLOAT_ERROR_MARGIN) {
NSTimeInterval toggleDuration = _startVelocity? MIN(1.0, MAX(0.0, 1.0 / fabs(_startVelocity))): 1.0;
toggleDuration = MIN_TOGGLE_DURATION + (MAX_TOGGLE_DURATION - MIN_TOGGLE_DURATION) * toggleDuration;
NSTimeInterval time = MIN(1.0, (currentTime - _toggleTime) / toggleDuration);
delta = [self easeInOut:time];
_toggle = (_toggle < 0.0)? (delta - 1.0): (1.0 - delta);
[self didScroll];
} else if (!_autoscroll) {
[self stopAnimation];
}
[self popAnimationState];
}
- (CGFloat)easeInOut:(CGFloat)time
{
return (time < 0.5)? 0.5 * pow(time * 2.0, 3.0): 0.5 * pow(time * 2.0 - 2.0, 3.0) + 1.0;
}
- (CGFloat)decelerationDistance
{
CGFloat acceleration = -_startVelocity * DECELERATION_MULTIPLIER * (1.0 - _decelerationRate);
return -pow(_startVelocity, 2.0) / (2.0 * acceleration);
}
- (void)startDecelerating
{
CGFloat distance = [self decelerationDistance];
_startOffset = _scrollOffset;
_endOffset = _startOffset + distance;
if (_pagingEnabled) {
if (distance > 0.0) {
_endOffset = ceil(_startOffset);
} else {
_endOffset = floor(_startOffset);
}
} else if (_stopAtItemBoundary) {
if (distance > 0.0) {
_endOffset = ceil(_endOffset);
} else {
_endOffset = floor(_endOffset);
}
}
if (!_wrapEnabled) {
if (_bounces) {
_endOffset = MAX(-_bounceDistance, MIN(_numberOfItems - 1.0 + _bounceDistance, _endOffset));
} else {
_endOffset = [self clampedOffset:_endOffset];
}
}
distance = _endOffset - _startOffset;
_startTime = CACurrentMediaTime();
_scrollDuration = fabs(distance) / fabs(0.5 * _startVelocity);
if (distance != 0.0) {
_decelerating = YES;
[self startAnimation];
}
}
- (BOOL)shouldDecelerate
{
return (fabs(_startVelocity) > SCROLL_SPEED_THRESHOLD) &&
(fabs([self decelerationDistance]) > DECELERATE_THRESHOLD);
}
- (BOOL)shouldScroll
{
return (fabs(_startVelocity) > SCROLL_SPEED_THRESHOLD) &&
(fabs(_scrollOffset - self.currentItemIndex) > SCROLL_DISTANCE_THRESHOLD);
}
- (void)didScroll
{
if (_wrapEnabled || !_bounces) {
_scrollOffset = [self clampedOffset:_scrollOffset];
} else {
CGFloat min = -_bounceDistance;
CGFloat max = MAX(_numberOfItems - 1, 0.0) + _bounceDistance;
if (_scrollOffset < min) {
_scrollOffset = min;
_startVelocity = 0.0;
} else if (_scrollOffset > max) {
_scrollOffset = max;
_startVelocity = 0.0;
}
}
//check if index has changed
NSInteger difference = [self minScrollDistanceFromIndex:self.currentItemIndex toIndex:self.previousItemIndex];
if (difference) {
_toggleTime = CACurrentMediaTime();
_toggle = MAX(-1, MIN(1, difference));
[self startAnimation];
}
[self loadUnloadViews];
[self transformItemViews];
//notify delegate of offset change
if (fabs(_scrollOffset - _previousScrollOffset) > 0.000001)
{
[self pushAnimationState:YES];
[_delegate sliderNeighborDidScroll:self];
[self popAnimationState];
}
//notify delegate of index change
if (_previousItemIndex != self.currentItemIndex)
{
[self pushAnimationState:YES];
if([_delegate respondsToSelector:@selector(sliderNeighborCurrentItemIndexDidChange:from:to:)]) {
[_delegate sliderNeighborCurrentItemIndexDidChange:self from:_previousItemIndex to:self.currentItemIndex];
}
[self popAnimationState];
}
//update previous index
_previousScrollOffset = _scrollOffset;
_previousItemIndex = self.currentItemIndex;
[self.indicator setCurrentPoint:self.currentItemIndex];
}
- (void)startAnimation
{
if (!_timer) {
self.timer = [NSTimer timerWithTimeInterval:1.0/60.0
target:self
selector:@selector(step)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:UITrackingRunLoopMode];
}
}
- (void)stopAnimation
{
[_timer invalidate];
_timer = nil;
}
#pragma mark -
#pragma mark view management
- (NSArray *)indexesForVisibleItems
{
return [[_itemViews allKeys] sortedArrayUsingSelector:@selector(compare:)];
}
- (NSArray *)visibleItemViews
{
NSArray *indexes = [self indexesForVisibleItems];
return [_itemViews objectsForKeys:indexes notFoundMarker:[NSNull null]];
}
- (UIView *)itemViewAtIndex:(NSInteger)index
{
return _itemViews[@(index)];
}
- (UIView *)currentItemView
{
return [self itemViewAtIndex:self.currentItemIndex];
}
- (NSInteger)indexOfItemView:(UIView *)view
{
NSInteger index = [[_itemViews allValues] indexOfObject:view];
if (index != NSNotFound) {
return [[_itemViews allKeys][index] integerValue];
}
return NSNotFound;
}
- (NSInteger)indexOfItemViewOrSubview:(UIView *)view
{
NSInteger index = [self indexOfItemView:view];
if (index == NSNotFound && view != nil && view != _contentView) {
return [self indexOfItemViewOrSubview:view.superview];
}
return index;
}
- (UIView *)itemViewAtPoint:(CGPoint)point
{
for (UIView *view in [[[_itemViews allValues] sortedArrayUsingFunction:(NSInteger (*)(id, id, void *))sliderNeighorCompareViewDepth context:(__bridge void *)self] reverseObjectEnumerator])
{
if ([view.superview.layer hitTest:point]) {
return view;
}
}
return nil;
}
- (void)setItemView:(UIView *)view forIndex:(NSInteger)index
{
_itemViews[@(index)] = view;
}
- (void)removeViewAtIndex:(NSInteger)index
{
NSMutableDictionary *newItemViews = [NSMutableDictionary dictionaryWithCapacity:[_itemViews count] - 1];
for (NSNumber *number in [self indexesForVisibleItems])
{
NSInteger i = [number integerValue];
if (i < index) {
newItemViews[number] = _itemViews[number];
} else if (i > index) {
newItemViews[@(i - 1)] = _itemViews[number];
}
}
self.itemViews = newItemViews;
[self.indicator setPointCount:self.itemViews.count];
}
- (void)insertView:(UIView *)view atIndex:(NSInteger)index
{
NSMutableDictionary *newItemViews = [NSMutableDictionary dictionaryWithCapacity:[_itemViews count] + 1];
for (NSNumber *number in [self indexesForVisibleItems])
{
NSInteger i = [number integerValue];
if (i < index) {
newItemViews[number] = _itemViews[number];
} else {
newItemViews[@(i + 1)] = _itemViews[number];
}
}
if (view) {
[self setItemView:view forIndex:index];
}
self.itemViews = newItemViews;
[self.indicator setPointCount:self.itemViews.count];
}
#pragma mark -
#pragma mark View loading
- (UIView *)loadViewAtIndex:(NSInteger)index withContainerView:(UIView *)containerView
{
[self pushAnimationState:NO];
UIView *view = nil;
if (index < 0) {
view = [_dataSource sliderNeighbor:self placeholderViewAtIndex:(NSInteger)(ceil((CGFloat)_numberOfPlaceholdersToShow/2.0)) + index reusingView:[self dequeuePlaceholderView]];
} else if (index >= _numberOfItems) {
view = [_dataSource sliderNeighbor:self placeholderViewAtIndex:_numberOfPlaceholdersToShow/2.0 + index - _numberOfItems reusingView:[self dequeuePlaceholderView]];
} else {
view = [_dataSource sliderNeighbor:self viewForItemAtIndex:index reusingView:[self dequeueItemView]];
}
if (view == nil) {
view = [[UIView alloc] init];
}
[self setItemView:view forIndex:index];
if (containerView) {
//get old item view
UIView *oldItemView = [containerView.subviews lastObject];
if (index < 0 || index >= _numberOfItems) {
[self queuePlaceholderView:oldItemView];
} else {
[self queueItemView:oldItemView];
}
//set container frame
CGRect frame = containerView.bounds;
if(_vertical) {
frame.size.width = view.frame.size.width;
frame.size.height = MIN(_itemWidth, view.frame.size.height);
} else {
frame.size.width = MIN(_itemWidth, view.frame.size.width);
frame.size.height = view.frame.size.height;
}
containerView.bounds = frame;
//set view frame
frame = view.frame;
frame.origin.x = (containerView.bounds.size.width - frame.size.width) / 2.0;
frame.origin.y = (containerView.bounds.size.height - frame.size.height) / 2.0;
view.frame = frame;
//switch views
[oldItemView removeFromSuperview];
[containerView addSubview:view];
} else {
[_contentView addSubview:[self containView:view]];
}
view.superview.layer.opacity = 0.0;
[self transformItemView:view atIndex:index];
[self popAnimationState];
return view;
}
- (UIView *)loadViewAtIndex:(NSInteger)index
{
return [self loadViewAtIndex:index withContainerView:nil];
}
- (void)loadUnloadViews
{
//set item width
[self updateItemWidth];
//update number of visible items
[self updateNumberOfVisibleItems];
//calculate visible view indices
NSMutableSet *visibleIndices = [NSMutableSet setWithCapacity:_numberOfVisibleItems];
NSInteger min = -(NSInteger)(ceil((CGFloat)_numberOfPlaceholdersToShow/2.0));
NSInteger max = _numberOfItems - 1 + _numberOfPlaceholdersToShow/2;
NSInteger offset = self.currentItemIndex - _numberOfVisibleItems/2;
if (!_wrapEnabled) {
offset = MAX(min, MIN(max - _numberOfVisibleItems + 1, offset));
}
for (NSInteger i = 0; i < _numberOfVisibleItems; i++)
{
NSInteger index = i + offset;
if (_wrapEnabled) {
index = [self clampedIndex:index];
}
CGFloat alpha = [self alphaForItemWithOffset:[self offsetForItemAtIndex:index]];
if (alpha) {
//only add views with alpha > 0
[visibleIndices addObject:@(index)];
}
}
//remove offscreen views
for (NSNumber *number in [_itemViews allKeys])
{
if (![visibleIndices containsObject:number]) {
UIView *view = _itemViews[number];
if ([number integerValue] < 0 || [number integerValue] >= _numberOfItems) {
[self queuePlaceholderView:view];
} else {
[self queueItemView:view];
}
[view.superview removeFromSuperview];
[(NSMutableDictionary *)_itemViews removeObjectForKey:number];
}
}
//add onscreen views
for (NSNumber *number in visibleIndices)
{
UIView *view = _itemViews[number];
if (view == nil) {
[self loadViewAtIndex:[number integerValue]];
}
}
}
#pragma mark View queing
- (void)queueItemView:(UIView *)view
{
if (view) {
[_itemViewPool addObject:view];
}
}
- (void)queuePlaceholderView:(UIView *)view
{
if (view) {
[_placeholderViewPool addObject:view];
}
}
- (UIView *)dequeueItemView
{
UIView *view = [_itemViewPool anyObject];
if (view) {
[_itemViewPool removeObject:view];
}
return view;
}
- (UIView *)dequeuePlaceholderView
{
UIView *view = [_placeholderViewPool anyObject];
if (view) {
[_placeholderViewPool removeObject:view];
}
return view;
}
- (void)setIndicator:(WXIndicatorView *)indicator
{
_indicator = indicator;
[_indicator setPointCount:self.numberOfItems];
[_indicator setCurrentPoint:[self currentItemIndex]];
}
- (void)layoutSubviews
{
[super layoutSubviews];
_contentView.frame = self.bounds;
[self layOutItemViews];
}
@end
@implementation NSObject (WXSliderNeighborView)
- (NSUInteger)numberOfPlaceholdersInSliderNeighborView:(__unused WXSliderNeighborView *)sliderNeighbor { return 0; }
- (void)sliderNeighborWillBeginScrollingAnimation:(__unused WXSliderNeighborView *)sliderNeighbor {}
- (void)sliderNeighborDidEndScrollingAnimation:(__unused WXSliderNeighborView *)sliderNeighbor {}
- (void)sliderNeighborDidScroll:(__unused WXSliderNeighborView *)sliderNeighbor {}
- (void)sliderNeighborCurrentItemIndexDidChange:(__unused WXSliderNeighborView *)sliderNeighbor {}
- (void)sliderNeighborWillBeginDragging:(__unused WXSliderNeighborView *)sliderNeighbor {}
- (void)sliderNeighborDidEndDragging:(__unused WXSliderNeighborView *)sliderNeighbor willDecelerate:(__unused BOOL)decelerate {}
- (void)sliderNeighborWillBeginDecelerating:(__unused WXSliderNeighborView *)sliderNeighbor {}
- (void)sliderNeighborDidEndDecelerating:(__unused WXSliderNeighborView *)sliderNeighbor {}
- (BOOL)sliderNeighbor:(__unused WXSliderNeighborView *)sliderNeighbor shouldSelectItemAtIndex:(__unused NSInteger)index { return YES; }
- (void)sliderNeighbor:(__unused WXSliderNeighborView *)sliderNeighbor didSelectItemAtIndex:(__unused NSInteger)index {}
- (CGFloat)sliderNeighborItemWidth:(__unused WXSliderNeighborView *)sliderNeighbor { return 0; }
- (CATransform3D)sliderNeighbor:(__unused WXSliderNeighborView *)sliderNeighbor
itemTransformForOffset:(__unused CGFloat)offset
baseTransform:(CATransform3D)transform { return transform; }
- (CGFloat)sliderNeighbor:(__unused WXSliderNeighborView *)sliderNeighbor
valueForOption:(__unused WXSliderNeighborOption)option
withDefault:(CGFloat)value { return value; }
@end
@interface WXSliderNeighborComponent () <WXIndicatorComponentDelegate>{
WXPixelType neighborSpace;
CGFloat neighborAlpha;
CGFloat neighborScale;
CGFloat currentItemScale;
}
@property (nonatomic, strong) WXSliderNeighborView *sliderView;
@property (nonatomic, assign) BOOL autoPlay;
@property (nonatomic, assign) NSUInteger interval;
@property (nonatomic, assign) NSInteger index;
@property (nonatomic, strong) NSMutableArray *items;
@property (nonatomic, strong) NSTimer *autoTimer;
@property (nonatomic, assign) BOOL sliderChangeEvent;
@property (nonatomic, assign) NSInteger currentIndex;
@property (nonatomic) CGRect itemRect;
@property (nonatomic, assign) BOOL scrollable;
@end
#define DEFAULT_NEIGHBOR_ITEM_SCALE 0.8
#define DEFAULT_CURRENT_ITEM_SCALE 0.9
#define DEFAULT_NEIGHBOR_ALPHA 0.6
#define DEFAULT_ANIMATION_DURATION 0.3
#define DEFAULT_NEIGHBOR_SPACE 25
@implementation WXSliderNeighborComponent
- (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;
_interval = 3000;
_items = [NSMutableArray array];
_itemRect = CGRectNull;
self->neighborAlpha = DEFAULT_NEIGHBOR_ALPHA;
self->neighborScale = DEFAULT_NEIGHBOR_ITEM_SCALE;
self->currentItemScale = DEFAULT_CURRENT_ITEM_SCALE;
self->neighborSpace = [WXConvert WXPixelType:@(DEFAULT_NEIGHBOR_SPACE) scaleFactor:self.weexInstance.pixelScaleFactor];
_scrollable = YES;
[self setAttributes:attributes];
_scrollable = attributes[@"scrollable"] ? [WXConvert BOOL:attributes[@"scrollable"]] : YES;
}
self.flexCssNode->setFlexDirection(WeexCore::kFlexDirectionRow,NO);
return self;
}
#pragma mark life circle
- (UIView *)loadView
{
_sliderView = [[WXSliderNeighborView alloc] init];
return _sliderView;
}
- (void)dealloc
{
_sliderView.delegate = nil;
_sliderView.dataSource = nil;
if (_autoPlay) {
[self _stopAutoPlayTimer];
}
_sliderView = nil;
[self.items removeAllObjects];
}
- (void)viewDidUnload
{
[self.items removeAllObjects];
}
- (void)viewDidLoad
{
_sliderView = (WXSliderNeighborView *)self.view;
_sliderView.delegate = self;
_sliderView.dataSource = self;
_sliderView.contentView.clipsToBounds = YES;
_sliderView.scrollEnabled = _scrollable;
UIAccessibilityTraits traits = UIAccessibilityTraitAdjustable;
if (_autoPlay) {
traits |= UIAccessibilityTraitUpdatesFrequently;
[self _startAutoPlayTimer];
} else {
[self _stopAutoPlayTimer];
}
_sliderView.accessibilityTraits = traits;
}
- (void)layoutDidFinish {
[_sliderView setCurrentItemIndex:_index];
}
- (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;
WXSliderNeighborView *sliderView = (WXSliderNeighborView *)self.view;
if ([view isKindOfClass:[WXIndicatorView class]]) {
((WXIndicatorComponent *)subcomponent).delegate = self;
[sliderView addSubview:view];
return;
}
if(index < 0) {
[self.items addObject:view];
} else {
[self.items insertObject:view atIndex:index];
}
subcomponent.isViewFrameSyncWithCalculated = NO;
if (index == -1) {
[sliderView insertView:view atIndex:index];
} else {
NSInteger offset = 0;
for (int i = 0; i < [self.items count]; ++i) {
if (index == i) break;
if ([self.items[i] isKindOfClass:[WXIndicatorView class]]) {
offset++;
}
}
[sliderView insertView:view atIndex:index - offset];
}
[sliderView reloadData];
}
}
#pragma mark attributes update
- (void)setAttributes:(NSDictionary *)attributes
{
if (attributes[@"index"]) {
_index = [attributes[@"index"] integerValue];
_currentIndex = _index;
}
if (attributes[@"autoPlay"]) {
_autoPlay = [attributes[@"autoPlay"] boolValue];
if (_autoPlay) {
[self _startAutoPlayTimer];
} else {
[self _stopAutoPlayTimer];
}
}
if (attributes[@"interval"]) {
_interval = [attributes[@"interval"] integerValue];
[self _stopAutoPlayTimer];
if (_autoPlay) {
[self _startAutoPlayTimer];
}
}
if (attributes[@"neighborScale"]) {
[self setNeighborScale:attributes];
}
if (attributes[@"currentItemScale"]) {
[self setCurrentItemScale:attributes];
}
if (attributes[@"neighborAlpha"]) {
[self setNeighborAlpha:attributes];
}
if (attributes[@"neighborSpace"]) {
[self setNeighborSpace:attributes];
}
if (attributes[@"scrollable"]) {
_scrollable = attributes[@"scrollable"] ? [WXConvert BOOL:attributes[@"scrollable"]] : YES;
((WXSliderNeighborView *)self.view).scrollEnabled = _scrollable;
}
}
- (void)updateAttributes:(NSDictionary *)attributes
{
[self setAttributes:attributes];
[self.sliderView setCurrentItemIndex:_index];
[self updateSliderPage:YES];
}
#pragma mark styles update
- (void)updateStyles:(NSDictionary *)styles {
}
#pragma mark event
- (void)addEvent:(NSString *)eventName
{
if ([eventName isEqualToString:@"change"]) {
_sliderChangeEvent = YES;
}
}
- (void)removeEvent:(NSString *)eventName
{
if ([eventName isEqualToString:@"change"]) {
_sliderChangeEvent = NO;
}
}
#pragma mark WXIndicatorComponentDelegate Methods
- (void)setIndicatorView:(WXIndicatorView *)indicatorView
{
NSAssert(_sliderView, @""); //!OCLint
[_sliderView setIndicator:indicatorView];
}
- (void)setNeighborSpace:(NSDictionary *)attributes{
if(attributes[@"neighborSpace"]) {
self->neighborSpace = [WXConvert WXPixelType:attributes[@"neighborSpace"] scaleFactor:self.weexInstance.pixelScaleFactor];
}
}
- (void)setNeighborAlpha:(NSDictionary *)attributes {
if (attributes[@"neighborAlpha"]) {
self->neighborAlpha = [WXConvert CGFloat:attributes[@"neighborAlpha"]];
self->neighborAlpha = self->neighborAlpha >= 0 ? self->neighborAlpha : 0;
self->neighborAlpha = self->neighborAlpha <= 1 ? self->neighborAlpha: 1;
}
}
- (void)setCurrentItemScale:(NSDictionary *)attributes {
if (attributes[@"currentItemScale"]) {
self->currentItemScale = [WXConvert CGFloat:attributes[@"currentItemScale"]];
self->currentItemScale = self->currentItemScale >= 0 ? self->currentItemScale : 0;
self->currentItemScale = self->currentItemScale <= 1 ? self->currentItemScale: 1;
}
}
- (void)setNeighborScale:(NSDictionary *)attributes
{
if (attributes[@"neighborScale"]) {
self->neighborScale = [WXConvert CGFloat:attributes[@"neighborScale"]];
self->neighborScale = self->neighborScale >= 0? self->neighborScale : 0;
self->neighborScale = self->neighborScale <= 1? self->neighborScale :1;
}
}
#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
{
WXSliderNeighborView *sliderNeighborView = (WXSliderNeighborView *)self.view;
int indicatorCnt = 0;
for (int i = 0; i < [self.items count]; ++i) {
if ([self.items[i] isKindOfClass:[WXIndicatorView class]]) {
indicatorCnt++;
}
}
self.currentIndex ++;
if (self.currentIndex >= self.items.count - indicatorCnt) {
self.currentIndex = 0;
}
[sliderNeighborView setCurrentItemIndex:self.currentIndex];
}
#pragma mark sliderNeighbor Delegate && dataSource
- (NSInteger)numberOfItemsInSliderNeighbor:(WXSliderNeighborView *)sliderNeighbor {
return [self.items count];
}
- (NSInteger)numberOfPlaceholdersInsliderNeighbor:(WXSliderNeighborView *)sliderNeighbor {
return 2;
}
- (UIView *)sliderNeighbor:(WXSliderNeighborView *)sliderNeighbor viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view {
if (!view) {
if (index < [self.items count]) {
view = self.items[index];
}else {
return nil;
}
} else {
view.tag = 1;
}
if (CGRectIsNull(_itemRect)) {
_itemRect = view.frame;
}
if (index == [self.items count] - 1) {
[self updateSliderPage:NO];
}
return view;
}
- (BOOL)sliderNeighbor:(WXSliderNeighborView *)sliderNeighbor shouldSelectItemAtIndex:(NSInteger)index {
return YES;
}
- (UIView *)sliderNeighbor:(WXSliderNeighborView *)sliderNeighbor placeholderViewAtIndex:(NSInteger)index reusingView:(UIView *)view
{
return view;
}
- (void)sliderNeighborDidEndScrollingAnimation:(WXSliderNeighborView *)sliderNeighbor
{
[self updateSliderPage:YES];
}
- (void)sliderNeighbor:(WXSliderNeighborView *)sliderView didScrollToItemAtIndex:(NSInteger)index
{
self.currentIndex = index;
if (_sliderChangeEvent) {
[self fireEvent:@"change" params:@{@"index":@(index)} domChanges:@{@"attrs": @{@"index": @(index)}}];
}
}
- (void)sliderNeighborWillBeginDragging:(WXSliderNeighborView *)sliderNeighbor
{
[self _stopAutoPlayTimer];
}
- (void)sliderNeighborDidEndDragging:(WXSliderNeighborView *)sliderNeighbor willDecelerate:(BOOL)decelerate {
if (_autoPlay) {
[self _startAutoPlayTimer];
}
}
- (void)updateSliderPage:(BOOL)animate
{
__block UIView * currentView = [self.sliderView itemViewAtIndex:[_sliderView currentItemIndex]];
__block UIView * lastView = [self.sliderView itemViewAtIndex:[_sliderView lastItemIndex]];
__block UIView * nextView = [self.sliderView itemViewAtIndex:[_sliderView nextItemIndex]];
__block CGAffineTransform transfrom = CGAffineTransformIdentity;
float duration = 0;
__weak typeof(self) weakSelf = self;
if (animate) {
duration = DEFAULT_ANIMATION_DURATION;
}
[UIView animateWithDuration:duration animations:^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
currentView.alpha = 1.0;
if (fabs(strongSelf->currentItemScale) > CGFLOAT_MIN) {
transfrom = CGAffineTransformConcat(transfrom,CGAffineTransformMakeScale(strongSelf->currentItemScale, strongSelf->currentItemScale));
}
currentView.transform = transfrom;
transfrom = CGAffineTransformIdentity;
if (fabs(strongSelf->neighborScale) <= CGFLOAT_MIN) {
strongSelf->neighborScale = DEFAULT_NEIGHBOR_ITEM_SCALE;
}
CGFloat tx = 0.5*_itemRect.size.width*((1-self->neighborScale)+(1-self->currentItemScale))-self->neighborSpace;
transfrom = CGAffineTransformConcat(transfrom, CGAffineTransformMakeScale(strongSelf->neighborScale, strongSelf->neighborScale));
nextView.transform = CGAffineTransformConcat(transfrom, CGAffineTransformMakeTranslation(-tx, 0));
lastView.transform = CGAffineTransformConcat(transfrom, CGAffineTransformMakeTranslation(tx, 0));
lastView.alpha = strongSelf->neighborAlpha;
nextView.alpha = strongSelf->neighborAlpha;
}
}];
}
@end