// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2015-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import "PageView.h" #import "RectUtil.h" @interface PageView () @property PagingDirection direction; @property UIPanGestureRecognizer *panGesture; @end @implementation PageView - (id) initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // clip if not on entire screen if (self.superview != nil && self.superview.frame.size.width > self.frame.size.width) { self.clipsToBounds = YES; } _panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; [self addGestureRecognizer:_panGesture]; _pageGap = DEFAULT_PAGE_GAP; _parallaxFactor = DEFAULT_PARALLAX_FACTOR; } return self; } - (void)setBgView:(UIView *)bgView { _bgView = bgView; [self addSubview:_bgView]; [self sendSubviewToBack:_bgView]; } - (void) setHidden: (BOOL)hidden { [super setHidden: hidden]; _centerView.hidden = hidden; _rightView.hidden = hidden; _leftView.hidden = hidden; } - (void) setDatasource:(id)datasource { _datasource = datasource; [self reset]; } - (void) resetPageFrames { self.leftView.frame = self.bounds; self.centerView.frame = self.bounds; self.rightView.frame = self.bounds; BOOL animationsEnabled = [UIView areAnimationsEnabled]; [UIView setAnimationsEnabled:NO]; [self positionViews]; [UIView setAnimationsEnabled:animationsEnabled]; } - (void) reset { [self preparePagedViews]; [self positionViews]; } - (void) pageLeft { BOOL changePage = [self preparePageLeft]; [self positionViewsWithDuration: MAX_ANIMATION_DURATION changePage:changePage]; } - (void) pageRight { BOOL changePage = [self preparePageRight]; [self positionViewsWithDuration: MAX_ANIMATION_DURATION changePage:changePage]; } - (void)enablePanGesture:(BOOL)enablePan { _panGesture.enabled = enablePan; } #pragma mark - UIPanGestureRecognizer - (void) pan: (UIPanGestureRecognizer *) recognizer { if (recognizer.state == UIGestureRecognizerStateBegan) { } else if (recognizer.state == UIGestureRecognizerStateChanged) { CGPoint transformed = [recognizer translationInView:self]; [self moveAllViewsByX:transformed.x]; [recognizer setTranslation:CGPointZero inView:self]; } else if (recognizer.state == UIGestureRecognizerStateEnded) { BOOL changePage = NO; if ([self movedLeftAcrossTrigger] && [self checkSpeedTrigger:recognizer]) { changePage = [self preparePageLeft]; } else if ([self movedRightAcrossTrigger] && [self checkSpeedTrigger:recognizer]) { changePage = [self preparePageRight]; } CGPoint velocity = [recognizer velocityInView:self]; [self positionViewsWithVelocity: velocity changePage:changePage]; } } - (BOOL) checkSpeedTrigger: (UIPanGestureRecognizer *) recognizer { CGPoint velocity = [recognizer velocityInView:self]; return (fabs(velocity.x) > MIN_PAGE_PAN_SPEED); } - (BOOL) movedRightAcrossTrigger { return (self.centerView.frame.origin.x ) > MIN_PAGE_PAN_POINTS; } - (BOOL) movedLeftAcrossTrigger { return (self.centerView.frame.origin.x ) < -MIN_PAGE_PAN_POINTS; } - (void) moveAllViewsByX: (CGFloat) x { [self moveView:self.leftView X:x Y:0]; [self moveView:self.centerView X:x Y:0]; [self moveView:self.rightView X:x Y:0]; [self moveView:_bgView X:x*_parallaxFactor Y:0.0]; } - (void) moveView: (UIView *) view X: (CGFloat) x Y: (CGFloat) y { CGRect moved = view.frame; moved.origin.x += x; view.frame = moved; } - (void) placeView: (UIView *) view X: (CGFloat) x Y: (CGFloat) y { CGRect newPos = view.frame; newPos.origin.x = x; newPos.origin.y = y; view.frame = newPos; } - (BOOL) preparePageRight { BOOL canPage = [self.datasource moveToPrevious]; if (canPage == NO) { return NO; } [self.rightView removeFromSuperview]; self.rightView = self.centerView; self.centerView = self.leftView; self.leftView = [self.datasource previousView: self.bounds]; [self addSubview:self.leftView]; [self placeView:self.leftView X:-self.frame.size.width Y:self.leftView.frame.origin.y]; _direction = RIGHT; if ([_delegate respondsToSelector: @selector(willPageFrom:toView:)]) { [_delegate willPageFrom:self.rightView toView:self.centerView]; } return YES; } - (BOOL) preparePageLeft { BOOL canPage = [self.datasource moveToNext]; if (canPage == NO) { return NO; } [self.leftView removeFromSuperview]; self.leftView = self.centerView; self.centerView = self.rightView; self.rightView = [self.datasource nextView: self.bounds]; [self addSubview:self.rightView]; [self placeView:self.rightView X:self.frame.size.width Y:self.rightView.frame.origin.y]; _direction = LEFT; if ([_delegate respondsToSelector: @selector(willPageFrom:toView:)]) { [_delegate willPageFrom:self.leftView toView:self.centerView]; } return YES; } - (void) positionViews { CGFloat currentCenterX = self.centerView.frame.origin.x; [self placeView:self.leftView X:-(self.frame.size.width + _pageGap) Y:self.leftView.frame.origin.y]; [self placeView:self.centerView X:0.0 Y:self.centerView.frame.origin.y]; [self placeView:self.rightView X:(self.frame.size.width + _pageGap) Y:self.rightView.frame.origin.y]; CGFloat diff = self.centerView.frame.origin.x - currentCenterX; CGFloat newBgXOffset = self.bgView.frame.origin.x + diff * _parallaxFactor; [self placeView:self.bgView X:newBgXOffset Y:self.bgView.frame.origin.y]; } - (void) positionViewsWithVelocity: (CGPoint) velocity changePage:(BOOL)changePage { CGFloat points2move = fabs(self.centerView.frame.origin.x); CGFloat duration = points2move / fabs(velocity.x); duration = MIN(duration, MAX_ANIMATION_DURATION); [self positionViewsWithDuration: duration changePage:changePage]; } - (void) positionViewsWithDuration: (CGFloat) duration changePage:(BOOL)changePage { UIViewAnimationOptions options = UIViewAnimationCurveEaseOut | UIViewAnimationOptionBeginFromCurrentState; [UIView animateWithDuration:duration delay:0.0 options:options animations:^{ [self positionViews]; } completion:^(BOOL finished) { if (changePage) { [self notifyDelegate]; } }]; } - (void) preparePagedViews { for (UIView *view in [self subviews]) { [view removeFromSuperview]; } self.centerView = [self.datasource currentView: self.bounds]; [self addSubview:self.centerView]; self.leftView = [self.datasource previousView: self.bounds]; [self addSubview:self.leftView]; self.rightView = [self.datasource nextView: self.bounds]; [self addSubview:self.rightView]; } - (void) notifyDelegate { if ([_delegate respondsToSelector: @selector(didPageFrom:toView:)]) { if (_direction == RIGHT) { [_delegate didPageFrom:self.rightView toView:self.centerView]; } else if (_direction == LEFT) { [_delegate didPageFrom:self.leftView toView:self.centerView]; } } } - (CGSize) sizeThatFits:(CGSize)size { return [self.centerView sizeThatFits:size]; } @end