导航本质
导航条也是继承自UIViewController,它有自己的view,我们的view都是放在UIViewControllerWrapperView上面。所以如果我们要自定义一个导航条,可以继承自UIViewController,然后用一个数组管理我们的viewControllers,然后再把viewController的view加到我们定义的导航条的view上面。
导航分为三个区:导航区(nav.navigationBar),内容区(nav.viewControllers),工具区(nav.toolbar)
其中工具区默认被隐藏。
导航内容展示的几种区别
// 透明全局(默认)
- (void)translucentAndAll{
self.navigationController.navigationBar.translucent = YES;
self.edgesForExtendedLayout = UIRectEdgeAll;
// self.automaticallyAdjustsScrollViewInsets = YES;
// self.extendedLayoutIncludesOpaqueBars = NO;
}
// 透明64
- (void)translucentAnd64{
self.navigationController.navigationBar.translucent = YES;
self.edgesForExtendedLayout = UIRectEdgeNone; // 不拓展它的区域,从导航栏下面开始
}
// 不透明64
- (void)noTranslucentAnd64{
self.navigationController.navigationBar.translucent = NO;
// self.edgesForExtendedLayout = UIRectEdgeNone;
}
// 不透明全局
- (void)noTranslucentAndAll{
self.navigationController.navigationBar.translucent = NO;
self.extendedLayoutIncludesOpaqueBars = YES; // 延伸区域包括不透明的bar
}
导航条返回按钮既有图片也有文字
可以创建一个view,在view上面添加图片和文字,再调用self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:view];
这句代码,但是系统默认的会和最左边有20的像素
要解决这个20像素的问题,可以将view上添加的控件的x值设为负数,但是这样点击那小于20像素的部分就不会响应事件。如下:
view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
view.backgroundColor = [UIColor redColor];
UIImageView *imageV = [[UIImageView alloc] initWithFrame:CGRectMake(-10, 0, 20, 20)];
imageV.image = [UIImage imageNamed:@"arrow.png"];
[view addSubview:imageV];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:view];
注意这里只能是将view上添加的控件的x值设为负数,将view的x值设为负数,距离最左边还是20像素,没有意义。
所以最好的解决办法是将view直接添加到navigationBar上面[self.navigationController.navigationBar addSubview:view];
。然后在viewWillDisappear的方法中从父控件中移除,才不会影响到下一个界面。
导航条返回按钮只有图片
这里需要将图片渲染,并且只有将navigationBar的backIndicatorImage和backIndicatorTransitionMaskImage两个图片属性都设置为它才会有效果,如下:
UIImage *image = [UIImage imageNamed:@"arrow.png"];
//一定要加这句代码,出来的才是图片的颜色,不然一直系统的蓝色
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
self.navigationController.navigationBar.backIndicatorImage = image;
self.navigationController.navigationBar.backIndicatorTransitionMaskImage = image;
另外:self.navigationItem.backBarButtonItem设置的是当前界面用导航推过去的下一个界面的返回按钮。
设置导航栏为透明
self.navigationController.navigationBar.translucent属性必须为YES。
方法一:给navigationBar添加一个透明的背景图片,但是设置它的navigationBar.backgroundColor背景色无效
-(void)transluentStyle{
[self.navigationController.navigationBar setBackgroundImage:self.image forBarMetrics:UIBarMetricsDefault];
// 这个方法不行
// self.navigationController.navigationBar.backgroundColor = [UIColor clearColor];
}
- (UIImage *)image{
UIGraphicsBeginImageContext(CGSizeMake(100, 100));
[[[UIColor whiteColor] colorWithAlphaComponent:0] setFill];
UIRectFill(CGRectMake(0, 0, 100, 100));
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
方法二:遍历子控件,并将_UIBarBackground的控件背景设置为透明色
- (void)transluentTwoStyle{
NSArray *ary = [self.navigationController.navigationBar subviews];
UIColor *alphaColor = [[UIColor whiteColor] colorWithAlphaComponent:0];
for (int i = 0; i < ary.count; i++) {
UIView *view = ary[i];
view.backgroundColor = alphaColor;
for (int j = 0; j < view.subviews.count; j++) {
UIView *subView = view.subviews[j];
subView.backgroundColor = alphaColor;
for (int k = 0; k < subView.subviews.count; k++) {
UIView *subsubView = subView.subviews[k];
subsubView.backgroundColor = alphaColor;
}
}
if([view isKindOfClass:NSClassFromString(@"_UIBarBackground")]){
view.backgroundColor = alphaColor;
}
}
}
去掉导航下面的那条黑线
方法一:通过Debug View Hierarchy层级可以看到那根黑线的y坐标是64,在导航栏下面,所以我们只要如下就好
self.navigationController.navigationBar.clipsToBounds = YES;
方法二:老办法,递归遍历找到黑线并隐藏
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"黑线处理";
// 导航条背景色
self.navigationController.navigationBar.barTintColor = [UIColor yellowColor];
UIImageView *iamgeV = [self findBackLineImageV:self.navigationController.navigationBar];
iamgeV.hidden = YES;
}
- (UIImageView *)findBackLineImageV:(UIView*)view{
// 递归写法
// 找到了黑线就返回
if([view isKindOfClass:[UIImageView class]] && view.frame.size.height <= 1){
return (UIImageView*)view;
}
NSArray *viewAry = view.subviews;
// 如果不是黑线,就遍历它的子view,再走本身这个方法,找出黑线
for (int i = 0; i < viewAry.count; i++) {
UIView *tmpV = [self findBackLineImageV:viewAry[i]];
if (tmpV) {
return (UIImageView*)tmpV;
}
}
return nil;
}
导航上面Item按钮的距离设置
可以用系统的方法,在两个Item直接加一个空格Item(这种思维也可以用到其他不好控制的布局上面,当要改变两个直接的距离的时候,直接改变中间控件的宽高就可以了)
NSMutableArray *barItems = [NSMutableArray array];
UIBarButtonItem *barItem = [[UIBarButtonItem alloc] initWithTitle:@"Nav" style:UIBarButtonItemStylePlain target:self action:@selector(showNav)];
UIBarButtonItem *barItemSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
barItemSpace.width = 60;
UIBarButtonItem *barItemT = [[UIBarButtonItem alloc] initWithTitle:@"view" style:UIBarButtonItemStylePlain target:self action:@selector(showNavTwo)];
[barItems addObject:barItem];
[barItems addObject:barItemSpace];
[barItems addObject:barItemT];
self.navigationItem.rightBarButtonItems = barItems;
导航栏标题
我们也可以自己写一个view,赋给导航条的titleView,如下:
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(20, 0, 50, 50)];
view.backgroundColor = [UIColor redColor];
self.navigationItem.titleView = view;
其中这里设置view的x值为20也是没有意义的和leftBarButtonItem一样,它一样还是会居中显示
重点:其实很多有些复杂,用系统不方便的一些设置,我们完全可以不用系统的,直接写一个控件添加到navigationBar上面,然后再在viewWillDisappear的方法中从父控件中移除,不影响到下一个界面就好。
navigationBarHidden和navigationBar.hidden
self.navigationController.navigationBarHidden = YES;
self.navigationController.navigationBar.hidden = YES;
第一个方法是navigationController的属性,第二个是navigationBar本身的属性。其中第一个可以看做是执行可以看作是remove操作navigationBar操作[self.navigationController.navigationBar removeFromSuperview];
注意:两个隐藏的方法要分别对应使用,navigationBarHidden设置的为YES,当要显示的时候,也必须是将这个属性设置为NO,不能套用,不然会显示不出来。
导航栏的隐藏还有一种方法,就是通过代理方法来实现,设置self为导航控制器的代理,实现代理方法,在将要显示控制器中设置导航栏隐藏和显示:
@interface WLHomePageController () <UINavigationControllerDelegate>
@end
@implementation WLHomePageController
#pragma mark - lifeCycle
- (void)viewDidLoad {
[super viewDidLoad];
// 设置导航控制器的代理为self
self.navigationController.delegate = self;
}
#pragma mark - UINavigationControllerDelegate
// 将要显示控制器
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
// 判断要显示的控制器是否是自己
BOOL isShowHomePage = [viewController isKindOfClass:[self class]];
[self.navigationController setNavigationBarHidden:isShowHomePage animated:YES];
}
自己实现一个导航返回的动画效果
分析:我们需要用到手势,获取手势的移动距离,从而来进行操作和判断。有点类似于只有两个页面的scrollview的滚动,其中转场的地方就是scrollview,前一个控制器的view和当前控制器的view就相当于scrollview的上面添加的两个view,只是还要包括导航条的操作,不过导航条不太好操作。具体的代码如下,里面有详细的注释:
#import "EOCEOCAnimaTheoryVC.h"
#import "UIView+EOCFrame.h"
@interface EOCEOCAnimaTheoryVC (){
UIView *transferView;
UIView *preView;
UINavigationBar *preNavBar;
}
@end
@implementation EOCEOCAnimaTheoryVC
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"动画原理";
UIPanGestureRecognizer *panGes = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleNavTransition:)];
[self.view addGestureRecognizer:panGes];
}
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
transferView = [self.view superview]; // 它的父控件UIViewControllerWrapperView,就是导航条自己本身的view,其他的view变化都是通过添加在它本身上面的
if (self.navigationController.viewControllers.count >= 2) {
NSArray *viewCtrAry = self.navigationController.viewControllers;
preView = ((UIViewController*)[viewCtrAry objectAtIndex:viewCtrAry.count -2]).view; //导航条中的上一个控制器的view
}
}
- (void)handleNavTransition:(UIPanGestureRecognizer*)gesture{
CGPoint gapPoint = [gesture translationInView:gesture.view]; // 获取手势的偏移量
[gesture setTranslation:CGPointZero inView:gesture.view]; // 每次滑动完置为零,避免累加
float width = gapPoint.x;
//CGPoint posPoint = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan) { // 手势开始
[preView setFrame:CGRectMake(-preView.eocW, preView.eocOrigin.y, preView.eocW, preView.eocH)]; // 将preView添加到self.view的紧挨着左边,但是这时是在屏幕外
[transferView addSubview:preView];
[transferView bringSubviewToFront:self.view];
// preNavBar = [[UINavigationBar alloc] initWithFrame:CGRectMake(-[UIScreen mainScreen].bounds.size.width, 0, [UIScreen mainScreen].bounds.size.width, 64)]; // 设置前一个导航栏
// NSMutableArray *array = [NSMutableArray array];
// [array addObjectsFromArray:self.navigationController.navigationBar.items]; //获取navigationBar上所有的items
// [array removeLastObject]; // 移除掉当前navigationBar上的items,就是前一个navigationBar上面所有的items
// preNavBar.items = array; // 将items赋给前一个navigationBar
// [transferView addSubview:preNavBar]; // 添加到转场view上面
}else if(gesture.state == UIGestureRecognizerStateChanged) { // 手势开始变化
// 将preView和self.view在他们的父控件transferView上都向右移动手势移动的距离长度
[preView setFrame:CGRectMake(preView.eocOrigin.x + width, preView.eocOrigin.y, preView.eocW, preView.eocH)];
[self.view setFrame:CGRectMake(self.view.eocOrigin.x + width, self.view.eocOrigin.y, self.view.eocW, self.view.eocH)];
// // 移动当前导航栏
// self.navigationController.navigationBar.frame = ({
// CGRect rect = self.navigationController.navigationBar.frame;
// rect.origin.x = self.view.frame.origin.x + width;
// rect;
// });
// // 移动前一个导航栏
// preNavBar.frame = ({
// CGRect rect = preNavBar.frame;
// rect.origin.x = preNavBar.frame.origin.x + width;
// rect;
// });
}else{
if (self.view.eocOrigin.x > self.view.eocW/2) { // 当向右滑动超过一半时,屏幕中间还是显示preView,滑过去了
[UIView animateWithDuration:0.3 animations:^{
[preView setFrame:CGRectMake(0, preView.eocOrigin.y, preView.eocW, preView.eocH)]; //将前一个控制器view移动到屏幕上
[self.view setFrame:CGRectMake(self.view.eocW, self.view.eocOrigin.y, self.view.eocW, self.view.eocH)]; //将当前控制器view移动到屏幕外
} completion:^(BOOL finished) {
NSMutableArray *array = [NSMutableArray array];
[array addObjectsFromArray:self.navigationController.viewControllers];
[array removeLastObject]; //移除掉最后一个控制器,即当前控制器
self.navigationController.viewControllers = array;
[self.view removeFromSuperview]; //将最后一个控制器,即当前控制器的view从父控件中移除
}];
}else{ // 当向右滑动没有超过一半时,屏幕中间还是显示的self.view,没有滑过去
[UIView animateWithDuration:0.3 animations:^{
[preView setFrame:CGRectMake(-preView.eocW, preView.eocOrigin.y, preView.eocW, preView.eocH)];
[self.view setFrame:CGRectMake(0, self.view.eocOrigin.y, self.view.eocW, self.view.eocH)];
} completion:^(BOOL finished) {
[preView removeFromSuperview]; //将preView从父控件中移除
}];
}
}
}
@end
其中需要注意的是transferView转场的view就是导航条本身的view:
效果如图:
上面代码中注释的部分,是关于导航条的动画操作,但是会有许多问题,如果打开运行,会有下面的问题,还没深究。另外导航条也有自己的代理UINavigationBarDelegate和代理方法
另外导航条也有自己的代理UINavigationBarDelegate和代理方法
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPushItem:(UINavigationItem *)item; // called to push. return NO not to.
- (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item; // called at end of animation of push or immediately if not animated
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item; // same as push methods
- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item;
替换系统转场动画手势
导航条有个手势属性,我们可以替换掉它
根据打印我们可以看到该手势的方法名为handleNavigationTransition:
NSArray *targetsArr = [self.navigationController.interactivePopGestureRecognizer valueForKey:@"targets"];
id target = [[targetsArr lastObject] valueForKey:@"target"];
SEL actionSEL = NSSelectorFromString(@"handleNavigationTransition:");
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:target action:actionSEL];
// [self.navigationController.interactivePopGestureRecognizer.view addGestureRecognizer:panGesture]; // 导航栏侧滑手势都被替换掉
[self.view addGestureRecognizer:panGesture];//只是当前页面被替换掉
通过系统的代理模式来写转场动画效果
导航条有自己的代理UINavigationControllerDelegate,我们可以重写它的代理方法来做操作,不然就是执行系统自己的动画效果
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC NS_AVAILABLE_IOS(7_0);
其中fromVC为当前控制器,toVC为目标控制器
我们重写这个方法时,由于返回的是一个支持UIViewControllerAnimatedTransitioning协议的对象,而系统没有,所以我们需要自己写一个继承自UIViewControllerAnimatedTransitioning协议的对象。这个协议还需要实现以下两个方法:
// This is used for percent driven interactive transitions, as well as for
// container controllers that have companion animations that might need to
// synchronize with the main animation.
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// This method can only be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
而最后一个方法里面又有一个协议UIViewControllerContextTransitioning,我们可以在协议中看到传到这个方法的对象transitionContext的一些属性和一些方法。
在最后一个方法打断点输出如图:
其中他的containerView属性对象其实就是导航条的view—— UIViewControllerWrapperView。并通过viewControllerForKey拿到当前控制器和目标控制器,然后我们用拿到的这些控制器在这个方法中实现自己的动画效果。
代码如下:
继承自UINavigationController的类
#import "EOCNavigationCtr.h"
#import "EOCNavAnimation.h"
@interface EOCNavigationCtr ()<UINavigationControllerDelegate, UIGestureRecognizerDelegate>{
UIPercentDrivenInteractiveTransition *_eocAnimaTransProgress;
}
@end
@implementation EOCNavigationCtr
- (void)viewDidLoad {
[super viewDidLoad];
self.delegate = self;
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleNavTransition:)];
panGesture.delegate = self;
[self.view addGestureRecognizer:panGesture];
}
- (void)handleNavTransition:(UIPanGestureRecognizer*)gesture{
CGPoint gapPoint = [gesture translationInView:gesture.view];
//[gesture setTranslation:CGPointZero inView:gesture.view];
float width = gapPoint.x;
float percent = width/[UIScreen mainScreen].bounds.size.width; // 动画完成进度
if (gesture.state == UIGestureRecognizerStateBegan) {
_eocAnimaTransProgress = [[UIPercentDrivenInteractiveTransition alloc] init];
[self popViewControllerAnimated:YES];
}else if(gesture.state == UIGestureRecognizerStateChanged) {
[_eocAnimaTransProgress updateInteractiveTransition:percent]; // 更新进度
}else{
if (percent > 0.5) {
[_eocAnimaTransProgress finishInteractiveTransition]; // 大于百分之五十就完成这个动画
}else{
[_eocAnimaTransProgress cancelInteractiveTransition]; // 小于百分之五十就不完成这个动画
}
_eocAnimaTransProgress = nil; // 置空
}
}
// 转场动画
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC{
if (operation == UINavigationControllerOperationPop) { // 操作为pop
EOCNavAnimation *eocNav = [[EOCNavAnimation alloc] init];
eocNav.nav = self;
return eocNav;
}
return nil;
}
// 控制动画进度
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController{
if ([animationController isKindOfClass:[EOCNavAnimation class]]) { //如果是这个动画就返回它的动画进度
return _eocAnimaTransProgress;
}
return nil;
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
/**
* 这里有两个条件不允许手势执行,1、当前控制器为根控制器;2、如果这个push、pop动画正在执行(私有属性)
*/
return self.viewControllers.count != 1 && ![[self valueForKey:@"_isTransitioning"] boolValue];
}
@end
继承自UIViewControllerAnimatedTransitioning协议的对象
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface EOCNavAnimation : NSObject<CAAnimationDelegate,UIViewControllerAnimatedTransitioning>{
id <UIViewControllerContextTransitioning> _transitionContext;
}
@property (nonatomic, strong)UINavigationController *nav;
@end
#import "EOCNavAnimation.h"
@implementation EOCNavAnimation
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{
return 0.5; // 返回动画时间
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
_transitionContext = transitionContext;
UIView *containView = transitionContext.containerView;
UIViewController *fromViewCtr = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewCtr = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
[containView insertSubview:toViewCtr.view belowSubview:fromViewCtr.view];
/*
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewCtr.view.transform = CGAffineTransformMakeTranslation([UIScreen mainScreen].bounds.size.width, 0);
} completion:^(BOOL finished) {
[_transitionContext completeTransition:![_transitionContext transitionWasCancelled]];
}];
*/
//复杂动画
CATransition *st = [CATransition animation];
st.type = @"cube";
st.subtype = @"fromLeft";
st.duration = 0.5;
st.removedOnCompletion = NO;
st.fillMode = kCAFillModeForwards;
st.delegate = self; // 设置代理,在代理方法中做完成标识,不然返回后不能做任何操作
[containView.layer addAnimation:st forKey:nil];
[containView exchangeSubviewAtIndex:0 withSubviewAtIndex:1];
}
// [transitionContext completeTransition:YES]; 没有做completeTransition 完成标识,任何动作都认为在做转场
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
[_transitionContext completeTransition:![_transitionContext transitionWasCancelled]];
}
- (void)animationEnded:(BOOL) transitionCompleted{
NSLog(@"animationEnded");
}
@end
参考文章
iOS导航栏的正确隐藏方式