新版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]; //將另一個視圖調到最下面 [self _configureViewShadowWithDirection:SliderMoveDirectionLeft]; //設置陰影 [UIView animateWithDuration:self.rightOpenDuration delay:0 usingSpringWithDamping:kOpenSpringDamping //彈性效果 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) {……}