来自Leo的原创博客,转载请著名出处
我的StackOverflow
我的Github
https://github.com/LeoMobileDeveloper
本来想着用两种方式:继承UINavigationController和用Runtime动态修改UINavigationController的实现来完成这篇博客的。最近,看美剧有点分心,又忙着整理React Native的东西,用Runtime来实现的部份就还没写完。所以,这篇博客就拆分成两篇吧,今天先把继承UINavigationController的方式写出来,后面的在说。
写了个简单的库LHNavigationController
看看效果,提供了两种效果,分别模仿网易新闻和斗鱼
网易新闻
斗鱼
LHNavigationController
设置delegate
为self
,来重写交互式转场动画
通过为LHNavigationController
添加两个pan手势,来分别控制push/pop
LHViewController
是UIViewController
的子类,自带1个NavigationBar
LHTableViewController
继承自LHViewController
添加了1个Tableview,来给子类调用
所有Controller需要继承自LHViewController 或 LHTableViewController,对代码影响较大(这个缺点不可避免的)
NavigationBar没法透明(这个后续会解决)
UINavigationController有1个属性是delegate
@property(nonatomic, weak) id< UINavigationControllerDelegate > delegate
这是1个遵守UINavigationControllerDelegate
的对象,通过设置delegate,我们可以重新定义push/pop的转场动画。这个协议中,我们主要用到以下两个方法
- navigationController:animationControllerForOperation:fromViewController:toViewController:
- navigationController:interactionControllerForAnimationController:
其中,
第1个方法返回1个id<UIViewControllerAnimatedTransitioning>
对象。用来提供转场动画
第2个方法返回id<UIViewControllerInteractiveTransitioning>
对象。用来提供交互式转场的控制器
更直观的表述就是:第1个控制在转场的时候,两个viewController各自若何动画,第2个用来控制动画的进度
通过上文的描写我们知道,SDK给我们的接口是实现某个协议便可。那末,实现id<UIViewControllerAnimatedTransitioning>
的对象我们就能够单独创建1个类,方便复用。
由通过1个类来处理push/pop,所以我们需要辨别,定义1个枚举
typedef NS_ENUM(NSInteger,LHNavAnimatorOperation){
LHNavAnimatorOperationPush,
LHNavAnimatorOperationPop,
};
然后,接口定义看起来是这模样的
@interface LHNavAnimator : NSObject<UIViewControllerAnimatedTransitioning>
//初始化,设置push/pop,绑定的navigationController
-(instancetype)initWithDirection:(LHNavAnimatorOperation)direction navigation:(UINavigationController *)nav;
//方向
@property (assign,nonatomic)LHNavAnimatorOperation operation;
//绑定的navigationController,为了在转场结束/取消的时候禁用手势
@property (weak,nonatomic)UINavigationController * nav;
@end
UIViewControllerAnimatedTransitioning
协议规定要实现以下两个方法
//动画的时间,这里的transitionContext是转场上下文,由系统提供,通过这个来获得前后两个ViewController
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
return 0.3;
}
//实际的动画
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
//
}
Tips:交互式转场的动画最好用UIView层次的API来实现
然后,我们来看看动画的具体实现,这里动画的本质是
//获得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);
//系统默许的转场距离是全部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) {
//结束的时候,恢复状态 _nav.view.userInteractionEnabled = YES;
fromView.transform = CGAffineTransformIdentity;
toView.transform = CGAffineTransformIdentity;
//通知转场上下文,转场结束
BOOL canceled = [transitionContext transitionWasCancelled];
[transitionContext completeTransition:!canceled];
}];
}else{
//...
}
上文提到了,为了自定义交互式转场,我们还需要返回这样1个对象
id<UIViewControllerInteractiveTransitioning>
庆幸的是,系统为我们提供了1个类UIPercentDrivenInteractiveTransition
,通常我们只需要用这个类或继承便可。主要用到以下3个方法
//更新转场进度,比如0.5表示转场进行了1半
updateInteractiveTransition:
//转场取消了
cancelInteractiveTransition
//转场结束了
finishInteractiveTransition
由于,有些转场是按键驱动,其实不是手势拖动,所以要支持非交互式转场。我们保存1个属性
@property (assign,nonatomic)BOOL isInteractive;
然后,交互式转场的时候,就反悔self.transition(UIPercentDrivenInteractiveTransition)对象
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController{
return _isInteractive ? self.transition : nil;
}
UINavigationControllerDelegate协议需要的两个对象我们准备好了,接下来需要手势来驱动转场了。这里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) {
//根据手势移动,更新转场进度
[self.transition updateInteractiveTransition:pec];
}else if (sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateCancelled) {
//手势结束的时候,根据速度判断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如何实现自己自带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还没有掉用的时候,进行属性设置无效
然后,在ViewDidLoad中,添加NavigationBar,添加lh_view作为容器,设置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;
}
这里的设置是通过self.view作为StatusBar背风景的填充,所以设置的时候,应当是这么设置的
- (void)setBarTintColor:(UIColor *)barTintColor{
_barTintColor = barTintColor;
self.view.backgroundColor = barTintColor;
self.lh_navigationBar.barTintColor = barTintColor;
}
其实实现像网易新闻那样pop,还有1个实现方式。
这类方式的原理以下
这样,每次push的时候,都push1个containViewController,而根NavigationController的导航栏是隐藏的。
这时候候,每个ViewController的层次架构以下
… RootNavigationController
……ContainViewController
………NavigationController
…………业务Controller
前段时间自己独立开发的这个项目就是用的这类视图架构。
你需要1个Manager来处理对应的逻辑,减少代码量。配合页面路由的技术架构,使用起来更好。
这两种App的架构,合适从App 的初始阶段使用。由于,
对全部的视图控制器的架构影响都很大。这里写出来,作为1种思路吧。
上一篇 awk入门学习笔记
下一篇 ios多线程 -- GCD介绍