程序员人生 网站导航

实现像网易新闻那样全屏push/pop

栏目:综合技术时间:2016-07-13 08:32:29

来自Leo的原创博客,转载请著名出处

我的StackOverflow

profile for Leo on Stack Exchange, a network of free, community-driven Q&A sites

我的Github
https://github.com/LeoMobileDeveloper


目标效果

  • 全屏幕左滑-进行导航交互式推出
  • 全屏幕右滑-进行导航交互式推入
  • 修改默许的导航栏push/Pop


怨言几句

本来想着用两种方式:继承UINavigationController和用Runtime动态修改UINavigationController的实现来完成这篇博客的。最近,看美剧有点分心,又忙着整理React Native的东西,用Runtime来实现的部份就还没写完。所以,这篇博客就拆分成两篇吧,今天先把继承UINavigationController的方式写出来,后面的在说。


LHNavigationController

写了个简单的库LHNavigationController
看看效果,提供了两种效果,分别模仿网易新闻和斗鱼

网易新闻

斗鱼

原理

  • LHNavigationController设置delegateself,来重写交互式转场动画

  • 通过为LHNavigationController添加两个pan手势,来分别控制push/pop

  • LHViewControllerUIViewController的子类,自带1个NavigationBar

  • LHTableViewController 继承自LHViewController添加了1个Tableview,来给子类调用

当前的代码缺点:

  1. 所有Controller需要继承自LHViewController 或 LHTableViewController,对代码影响较大(这个缺点不可避免的)

  2. NavigationBar没法透明(这个后续会解决)


UINavigationControllerDelegate

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个用来控制动画的进度


1个通用的Animator

通过上文的描写我们知道,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来实现

然后,我们来看看动画的具体实现,这里动画的本质是

  • UINavigationController提供1个ContainView作为动画的容器
  • 获得到两个View,1个是当前现实的View,1个是行将显示的View
  • 转场的本质就是当前显示View移除,行将显示View进入
//获得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; }

交互式push/Pop

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

然后,我们来看看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; }

StatusBar背风景

这里的设置是通过self.view作为StatusBar背风景的填充,所以设置的时候,应当是这么设置的

- (void)setBarTintColor:(UIColor *)barTintColor{ _barTintColor = barTintColor; self.view.backgroundColor = barTintColor; self.lh_navigationBar.barTintColor = barTintColor; }

实现方式2

其实实现像网易新闻那样pop,还有1个实现方式。

这类方式的原理以下

  • 每一个ViewController自带1个NavigationController
  • NavigationController作为childController添加到containViewController中

这样,每次push的时候,都push1个containViewController,而根NavigationController的导航栏是隐藏的。

这时候候,每个ViewController的层次架构以下

… RootNavigationController
……ContainViewController
………NavigationController
…………业务Controller

前段时间自己独立开发的这个项目就是用的这类视图架构。

你需要1个Manager来处理对应的逻辑,减少代码量。配合页面路由的技术架构,使用起来更好。


总结

这两种App的架构,合适从App 的初始阶段使用。由于,

第1种

  • 你所有的类都需要继承自LHViewController.
  • 你添加SubView,删除subview的时候,需要通过self.lh_view来实现。

第2种模式

  • 你所有的Controller都需要自带1个NavigationController,并且放到1个容器里

对全部的视图控制器的架构影响都很大。这里写出来,作为1种思路吧。

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐