來自Leo的原創(chuàng)博客,轉(zhuǎn)載請著名出處
我的StackOverflow
我的Github
https://github.com/LeoMobileDeveloper
本來想著用兩種方式:繼承UINavigationController和用Runtime動態(tài)修改UINavigationController的實現(xiàn)來完成這篇博客的。最近,看美劇有點分心,又忙著整理React Native的東西,用Runtime來實現(xiàn)的部份就還沒寫完。所以,這篇博客就拆分成兩篇吧,今天先把繼承UINavigationController的方式寫出來,后面的在說。
寫了個簡單的庫LHNavigationController
看看效果,提供了兩種效果,分別模仿網(wǎng)易新聞和斗魚
網(wǎng)易新聞
斗魚
LHNavigationController
設(shè)置delegate
為self
,來重寫交互式轉(zhuǎn)場動畫
通過為LHNavigationController
添加兩個pan手勢,來分別控制push/pop
LHViewController
是UIViewController
的子類,自帶1個NavigationBar
LHTableViewController
繼承自LHViewController
添加了1個Tableview,來給子類調(diào)用
所有Controller需要繼承自LHViewController 或 LHTableViewController,對代碼影響較大(這個缺點不可避免的)
NavigationBar沒法透明(這個后續(xù)會解決)
UINavigationController有1個屬性是delegate
@property(nonatomic, weak) id< UINavigationControllerDelegate > delegate
這是1個遵守UINavigationControllerDelegate
的對象,通過設(shè)置delegate,我們可以重新定義push/pop的轉(zhuǎn)場動畫。這個協(xié)議中,我們主要用到以下兩個方法
- navigationController:animationControllerForOperation:fromViewController:toViewController:
- navigationController:interactionControllerForAnimationController:
其中,
第1個方法返回1個id<UIViewControllerAnimatedTransitioning>
對象。用來提供轉(zhuǎn)場動畫
第2個方法返回id<UIViewControllerInteractiveTransitioning>
對象。用來提供交互式轉(zhuǎn)場的控制器
更直觀的表述就是:第1個控制在轉(zhuǎn)場的時候,兩個viewController各自若何動畫,第2個用來控制動畫的進度
通過上文的描寫我們知道,SDK給我們的接口是實現(xiàn)某個協(xié)議便可。那末,實現(xiàn)id<UIViewControllerAnimatedTransitioning>
的對象我們就能夠單獨創(chuàng)建1個類,方便復用。
由通過1個類來處理push/pop,所以我們需要辨別,定義1個枚舉
typedef NS_ENUM(NSInteger,LHNavAnimatorOperation){
LHNavAnimatorOperationPush,
LHNavAnimatorOperationPop,
};
然后,接口定義看起來是這模樣的
@interface LHNavAnimator : NSObject<UIViewControllerAnimatedTransitioning>
//初始化,設(shè)置push/pop,綁定的navigationController
-(instancetype)initWithDirection:(LHNavAnimatorOperation)direction navigation:(UINavigationController *)nav;
//方向
@property (assign,nonatomic)LHNavAnimatorOperation operation;
//綁定的navigationController,為了在轉(zhuǎn)場結(jié)束/取消的時候禁用手勢
@property (weak,nonatomic)UINavigationController * nav;
@end
UIViewControllerAnimatedTransitioning
協(xié)議規(guī)定要實現(xiàn)以下兩個方法
//動畫的時間,這里的transitionContext是轉(zhuǎn)場上下文,由系統(tǒng)提供,通過這個來獲得前后兩個ViewController
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
return 0.3;
}
//實際的動畫
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
//
}
Tips:交互式轉(zhuǎn)場的動畫最好用UIView層次的API來實現(xiàn)
然后,我們來看看動畫的具體實現(xiàn),這里動畫的本質(zhì)是
//獲得ViewController/fromview/toview/containview
UIViewController * fromvc = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController * tovc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView * fromView = fromvc.view;
UIView * toView = tovc.view;
UIView * containView = [transitionContext containerView];
CGFloat duration = [self transitionDuration:transitionContext];
CGFloat toTransition = CGRectGetWidth(containView.bounds);
//系統(tǒng)默許的轉(zhuǎn)場距離是全部containView的0.3
CGFloat fromTranstion = toTransition * 0.3;
//Add subview
[containView addSubview:toView];
if (_operation == LHNavAnimatorOperationPush) {
//禁用手勢
_nav.view.userInteractionEnabled = NO;
toView.transform = CGAffineTransformMakeTranslation(toTransition, 0);
fromView.transform = CGAffineTransformIdentity;
[containView bringSubviewToFront:toView];
//動畫很簡單,就是不同View相同時間移動距離不1樣,這樣移動的速度就不1樣
[UIView animateWithDuration:duration
delay:0.0
options:UIViewAnimationOptionCurveLinear
animations:^{
toView.transform = CGAffineTransformIdentity;
fromView.transform = CGAffineTransformMakeTranslation(-1 * fromTranstion, 0);
} completion:^(BOOL finished) {
//結(jié)束的時候,恢復狀態(tài) _nav.view.userInteractionEnabled = YES;
fromView.transform = CGAffineTransformIdentity;
toView.transform = CGAffineTransformIdentity;
//通知轉(zhuǎn)場上下文,轉(zhuǎn)場結(jié)束
BOOL canceled = [transitionContext transitionWasCancelled];
[transitionContext completeTransition:!canceled];
}];
}else{
//...
}
上文提到了,為了自定義交互式轉(zhuǎn)場,我們還需要返回這樣1個對象
id<UIViewControllerInteractiveTransitioning>
慶幸的是,系統(tǒng)為我們提供了1個類UIPercentDrivenInteractiveTransition
,通常我們只需要用這個類或繼承便可。主要用到以下3個方法
//更新轉(zhuǎn)場進度,比如0.5表示轉(zhuǎn)場進行了1半
updateInteractiveTransition:
//轉(zhuǎn)場取消了
cancelInteractiveTransition
//轉(zhuǎn)場結(jié)束了
finishInteractiveTransition
由于,有些轉(zhuǎn)場是按鍵驅(qū)動,其實不是手勢拖動,所以要支持非交互式轉(zhuǎn)場。我們保存1個屬性
@property (assign,nonatomic)BOOL isInteractive;
然后,交互式轉(zhuǎn)場的時候,就反悔self.transition(UIPercentDrivenInteractiveTransition)對象
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController{
return _isInteractive ? self.transition : nil;
}
UINavigationControllerDelegate協(xié)議需要的兩個對象我們準備好了,接下來需要手勢來驅(qū)動轉(zhuǎn)場了。這里push較為麻煩,主要講授push。
在LHNavigationController的ViewDidLoad中,添加push手勢
self.pushPan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePush:)];
self.pushPan.delegate = self;
self.pushPan.cancelsTouchesInView = NO;
[self.view addGestureRecognizer:self.pushPan];
在看看如何處理的手勢
- (void)handlePush:(UIScreenEdgePanGestureRecognizer *)sender{
//計算距離
CGFloat tx = [sender translationInView:self.view].x;
//計算進度
CGFloat pec = fabs(tx/CGRectGetWidth(self.view.frame));
//獲得速度
CGFloat vx = [sender velocityInView:self.view].x;
if (sender.state == UIGestureRecognizerStateBegan) {//手勢開始的時候,掉用代理來獲得下1個Controller,push到當前堆棧
self.isInteractive = YES;
UIViewController * nextvc = [self.lhDelegate viewControllerAfterController:self.viewControllers.lastObject];
[self pushViewController:nextvc animated:YES];
}else if (sender.state == UIGestureRecognizerStateChanged) {
//根據(jù)手勢移動,更新轉(zhuǎn)場進度
[self.transition updateInteractiveTransition:pec];
}else if (sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateCancelled) {
//手勢結(jié)束的時候,根據(jù)速度判斷push是不是成功
if (vx > 0) {//
[self.transition cancelInteractiveTransition];
}else{
[self.transition finishInteractiveTransition];
}
self.isInteractive = NO;
}
}
這里的lhDelegate是1個代理對象
@protocol LHNavigationControllerDelegate<NSObject>
//返回controller的下1個Controller
- (UIViewController *)viewControllerAfterController:(UIViewController *)controller;
@end
然后,我們來看看LHViewController如何實現(xiàn)自己自帶NavigationBar
聲明3個屬性
@property (strong,nonatomic,readonly)UINavigationBar * lh_navigationBar;
@property (strong,nonatomic,readonly)UINavigationItem * lh_navigationItem;
@property (strong,nonatomic,readonly)UIView * lh_view;
3個屬性都惰性初始化
- (UIView *)lh_view{
if (_lh_view == nil) {
_lh_view = [[UIView alloc] init];
}
return _lh_view;
}
//...
Tips:惰性初始化是為了避免在ViewDidLoad還沒有掉用的時候,進行屬性設(shè)置無效
然后,在ViewDidLoad中,添加NavigationBar,添加lh_view作為容器,設(shè)置AutoLayout
- (void)viewDidLoad{
[super viewDidLoad];
self.lh_navigationBar.translatesAutoresizingMaskIntoConstraints = NO;
self.lh_navigationBar.items = @[self.lh_navigationItem];
[self.view addSubview:_lh_navigationBar];
self.lh_view.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:_lh_view];
//束縛很簡單,可視化語言以下
//水平 H:|-0-[_lh_view]-0-|,H:|-0-[_lh_navigationBar]-0-|
//垂直 V:[topLayoutGuide]-0-[_lh_navigationBar]-0-[_lh_view]-0|
[self.view bringSubviewToFront:self.lh_navigationBar];
self.view.backgroundColor = [UIColor whiteColor];
self.lh_navigationBar.translucent = NO;
}
這里的設(shè)置是通過self.view作為StatusBar背風景的填充,所以設(shè)置的時候,應當是這么設(shè)置的
- (void)setBarTintColor:(UIColor *)barTintColor{
_barTintColor = barTintColor;
self.view.backgroundColor = barTintColor;
self.lh_navigationBar.barTintColor = barTintColor;
}
其實實現(xiàn)像網(wǎng)易新聞那樣pop,還有1個實現(xiàn)方式。
這類方式的原理以下
這樣,每次push的時候,都push1個containViewController,而根NavigationController的導航欄是隱藏的。
這時候候,每個ViewController的層次架構(gòu)以下
… RootNavigationController
……ContainViewController
………NavigationController
…………業(yè)務Controller
前段時間自己獨立開發(fā)的這個項目就是用的這類視圖架構(gòu)。
你需要1個Manager來處理對應的邏輯,減少代碼量。配合頁面路由的技術(shù)架構(gòu),使用起來更好。
這兩種App的架構(gòu),合適從App 的初始階段使用。由于,
對全部的視圖控制器的架構(gòu)影響都很大。這里寫出來,作為1種思路吧。
上一篇 awk入門學習筆記
下一篇 ios多線程 -- GCD介紹