新版QQ在UI方面做了不少更新,其中一个比较炫酷的效果就是其侧滑导航栏。虽然这种UI已经是被模仿来模仿去烂掉牙了,虽然有统计说这种设计的用户体验并不好。但是我本人还是非常喜欢这种效果的,于是跑去网上学习了一下。这里记录一下其实现。
一、效果展示:
二、实现思路
关系图:
如上图所示,主要思路如下:
1、SliderNavigation拥有三个子视图:leftView,rightView,mainView。左右滑动时就通过这三个视图之间层次关系的切换来实现。
2、其实只有上述三个视图完全够了,但是又另外加上了三个属性:leftVC,rightVC,mainVC。这样做的目的是简化操作,同时mainVC还有记录已展示过的视图的任务,这样所有视图都可以通过左右滑动唤出导航栏来了。这样每个子视图上展示的是对应控制器的视图,即[leftView addSubview:leftVC.view];,其他类似。
3、当向左滑动时,调整视图层级关系,因为向左滑动是展示右视图,所以将leftView调整到最底层,同时让mainView随手指移动,这样mainView之下的rightView就展示出来了。
4、有了上述三点,接下来就可以通过给各个环节添加动画来实现好看的效果了。
三、接口定义
.h文件中定义好外界可以自定义的一些属性。
首先是三个控制器
//左右控制器与主控制器 @property (strong, nonatomic) UIViewController *leftController; @property (strong, nonatomic) UIViewController *rightController; @property (strong, nonatomic) UIViewController *mainController;
//左右视图被拉出以后主视图的X方向的offset(正值) @property (assign, nonatomic) CGFloat leftOffsetX; @property (assign, nonatomic) CGFloat rightOffsetX; //左右视图被拉的过程中的判断点的X值(正值) @property (assign, nonatomic) CGFloat leftJudgeX; @property (assign, nonatomic) CGFloat rightJudegX; //左右视图拉出所用的时间 @property (assign, nonatomic) NSTimeInterval leftOpenDuration; @property (assign, nonatomic) NSTimeInterval rightOpenDuration; //左右视图收回时所用的时间 @property (assign, nonatomic) NSTimeInterval leftCloseDuration; @property (assign, nonatomic) NSTimeInterval rightCloseDuration; //左右视图被拉出以后主视图放缩的比例(0到1) @property (assign, nonatomic) CGFloat rightScale; @property (assign, nonatomic) CGFloat leftScale; //左右视图能否被拉出 @property (assign, nonatomic) BOOL canShowRight; @property (assign, nonatomic) BOOL canShowLeft;
为此我们设置一个字典来保存已经展示过的控制器
//用以记录被当做主控制器展示主视图过的控制器 @property (strong, nonatomic) NSMutableDictionary *controllersDict;
//单例 + (id)sharedInstance; //展示左右视图 - (void)showLeftView; - (void)showRightView; //展示自定义类的主视图,参数:自定义类名 - (void)showContentViewWithModel:(NSString *)className;
四、具体实现
首先定义一些常量
//制造反弹的动态效果,当通过按钮叫出导航栏时有效 static const CGFloat kOpenSpringDamping = 0.65f; static const CGFloat kOpenSpringVelocity = 0.10f; //定义常量表示拉动方向 typedef NS_ENUM(NSUInteger, sliderMoveDirection) { SliderMoveDirectionLeft = 0, SliderMoveDirectionRight, };
我们可以在初始化方法中将接口中声明的变量赋默认值,当用户没有为这些值赋值时便可以用这些默认值
首先我们初始化三个子视图为屏幕大小并根据添加到sliderNavigation的子视图中,注意添加顺序:我们希望让主视图在最上方,所以前两个随意,主视图必须最后添加。
- (void)_initSubviews { _rightView = [[UIView alloc] initWithFrame:self.view.bounds]; [self.view insertSubview:_rightView atIndex:0]; _leftView = [[UIView alloc] initWithFrame:self.view.bounds]; [self.view insertSubview:_leftView atIndex:1]; //主视图要最后添加(即添加到最上面显示) _mainView = [[UIView alloc] initWithFrame:self.view.bounds]; [self.view insertSubview:_mainView aboveSubview:_leftView]; }
在实现上述public方法“展示自定义类的主视图”时,传入参数为类名,将其作为键来从字典中取控制器,如果没有则以此类名新建一个控制器并加入到字典中。如果当前主视图上已经有视图,则将其移除。接着将自定义类的视图添加到mainView上,并相应赋值。
当然,不要忘了关闭左右导航栏(因为展示的类有可能是通过左右导航栏点出来的)
- (void)showContentViewWithModel:(NSString *)className { [self _closeSliderNavigation]; UIViewController *controller = [self.controllersDict objectForKey:className]; if (controller == nil) { Class c = NSClassFromString(className); controller = [[c alloc] init]; [self.controllersDict setObject:controller forKey:className]; } //如果当前已经有视图被显示,则将其取消 if (_mainView.subviews.count > 0) { [[_mainView.subviews firstObject] removeFromSuperview]; } controller.view.frame = _mainView.frame; [_mainView addSubview:controller.view]; self.mainController = controller; }
CGAffineTransform concat = [self _transformWithMoveDirection:SliderMoveDirectionLeft]; [self.view sendSubviewToBack:_leftView];<span style="white-space:pre"> </span> //将另一个视图调到最下面 [self _configureViewShadowWithDirection:SliderMoveDirectionLeft]; //设置阴影 [UIView animateWithDuration:self.rightOpenDuration delay:0 usingSpringWithDamping:kOpenSpringDamping<span style="white-space:pre"> </span> //弹性效果 initialSpringVelocity:kOpenSpringVelocity options:UIViewAnimationOptionCurveLinear animations:^{ _mainView.transform = concat; } completion:^(BOOL finished) { _showingLeft = NO; _showingRight = YES; self.mainController.view.userInteractionEnabled = NO; _tapGesture.enabled = YES; }];
最主要的还是滑动手势操作,也是比较麻烦的地方。不过其实思路比较清晰:获取偏移量,在滑动时计算出对应的变换矩阵并设置,在滑动结束时根据位置与判断点的关系做出相应的动画调整。
例如,滑动过程中向右拉时:
CGFloat translateX = [recognizer translationInView:_mainView].x; translateX += currentOffsetX; float scale = 0; //向右拉,展示的是左视图 if (translateX > 0) { if (self.canShowLeft == NO || self.leftController == nil) { return; } //将右视图放到底部以将左视图显示出来 [self.view sendSubviewToBack:_rightView]; [self _configureViewShadowWithDirection:SliderMoveDirectionRight]; if (_mainView.frame.origin.x < self.leftOffsetX) { scale = 1 - (_mainView.frame.origin.x / self.leftOffsetX) * (1 - self.leftScale); } else { scale = self.leftScale; } } else if (translateX < 0) {……}
而在拉动结束状态则与左拉右拉动画实现类似。
CGFloat translateX = [recognizer translationInView:_mainView].x; translateX += currentOffsetX; if (translateX > self.leftJudgeX) { if (self.canShowLeft == NO || self.leftController == nil) { return; } CGAffineTransform trans = [self _transformWithMoveDirection:SliderMoveDirectionRight]; [UIView beginAnimations:nil context:nil]; _mainView.transform = trans; [UIView commitAnimations]; _showingLeft = YES; _showingRight = NO; self.mainController.view.userInteractionEnabled = NO; _tapGesture.enabled = YES; } else if (translateX < -self.rightJudgeX) {……}
五、源码
附上源码:
ios侧边导航栏
参考代码:
参考代码
有任何问题欢迎提出,转载请注明