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

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

我的StackOverflow

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

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

目标效果

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

实现像网易新闻那样全屏push/pop_第1张图片

牢骚几句

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

LHNavigationController

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

网易新闻

斗鱼

原理

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

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

  • LHViewControllerUIViewController的子类,自带一个NavigationBar

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

当前的代码缺陷:

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

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

UINavigationControllerDelegate

UINavigationController有一个属性是delegate

@property(nonatomic, weak) id< UINavigationControllerDelegate > delegate

这是一个遵循UINavigationControllerDelegate的对象,通过设置delegate,我们可以重新定义push/pop的转场动画。这个协议中,我们主要用到以下两个方法

- navigationController:animationControllerForOperation:fromViewController:toViewController: - navigationController:interactionControllerForAnimationController: 

其中,

第一个方法返回一个id<UIViewControllerAnimatedTransitioning>对象。用来提供转场动画

第二个方法返回id<UIViewControllerInteractiveTransitioning>对象。用来提供交互式转场的控制器

更直观的表述就是:第一个控制在转场的时候,两个viewController各自如何动画,第二个用来控制动画的进度

一个通用的Animator

通过上文的描述我们知道,SDK给我们的接口是实现某个协议即可。那么,实现id<UIViewControllerAnimatedTransitioning>的对象我们就可以单独创建一个类,方便复用。

由通过一个类来处理push/pop,所以我们需要区分,定义一个枚举

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提供一个ContainView作为动画的容器
  • 获取到两个View,一个是当前现实的View,一个是即将显示的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相同时间移动距离不一样,这样移动的速度就不一样
        [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{
        //...
    }

交互式转场控制器

上文提到了,为了自定义交互式转场,我们还需要返回这样一个对象

id<UIViewControllerInteractiveTransitioning>

庆幸的是,系统为我们提供了一个类UIPercentDrivenInteractiveTransition,通常我们只需要用这个类或者继承即可。主要用到以下三个方法

//更新转场进度,比如0.5表示转场进行了一半
updateInteractiveTransition:
//转场取消了
cancelInteractiveTransition
//转场结束了
finishInteractiveTransition

由于,有些转场是按键驱动,并不是手势拖动,所以要支持非交互式转场。我们保存一个属性

@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) {//手势开始的时候,掉用代理来获取下一个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是一个代理对象

@protocol LHNavigationControllerDelegate<NSObject>
//返回controller的下一个Controller
- (UIViewController *)viewControllerAfterController:(UIViewController *)controller;
@end

LHViewController

然后,我们来看看LHViewController如何实现自己自带NavigationBar

声明三个属性

@property (strong,nonatomic,readonly)UINavigationBar * lh_navigationBar;
@property (strong,nonatomic,readonly)UINavigationItem * lh_navigationItem;
@property (strong,nonatomic,readonly)UIView * lh_view;

三个属性都惰性初始化

- (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;
}

实现方式二

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

这种方式的原理如下

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

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

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

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

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

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

总结

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

第一种

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

第二种模式

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

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

你可能感兴趣的:(全屏,Navigation,Léo,转场,网易新闻)