ios 导航栏(二)——自定义导航栏
程序员文章站
2024-03-20 19:48:04
...
目前主要的几种导航栏框架分为三种:
- 使用UINavigationController作为viewController的容器,即每次push的时候将viewController作为一个新的UINavigationController的根视图并管理当前viewcontroller。RTRootNavigationController
- 对系统的navigationBar进行隐藏或者设置透明,通过一个基类控制器添加一个navigationbar。
- 在做转场动画的时候判断两个控制器的navigationbar的样式是否一样,如果不一样就创建一个假的navigationbar。等视图加载完成后删除。
下图是一张A-push-B的方法调用的过程(来源:美团技术)
虚线内的情况会根据布局是否有变化而调用。
如果我们创建了一个自定义的导航栏组件系统,它的调用顺序可能会与此不同。
下图是B pop A 的流程图。
第三种方式
根据在做转场动画的时候根据navigationbar的style是否一致,来添加一张假的图。腾讯QMUI给出了一种方案,并提供四种方法来决定是否要fake导航栏来模拟。
- 判断是否实现了QMUINavigationControllerDelegate协议。该协议中会有外观样式、navigationbar显示隐藏与否。
- 判断fromVC和ToVC的customNavigationBarTransitionKey是否。返回的不一致就需要fakebar
- 配置表中的automaticCustomNavigationBarTransitionStyle,会设置每次push或者pop是否需要。
- fromVC和toVC的导航栏样式是否一致。
这个判断方法
- (BOOL)shouldCustomTransitionAutomaticallyWithFirstViewController:(UIViewController *)viewController1 secondViewController:(UIViewController *)viewController2 {
UIViewController<QMUINavigationControllerDelegate> *vc1 = (UIViewController<QMUINavigationControllerDelegate> *)viewController1;
UIViewController<QMUINavigationControllerDelegate> *vc2 = (UIViewController<QMUINavigationControllerDelegate> *)viewController2;
//1.
if (![vc1 conformsToProtocol:@protocol(QMUINavigationControllerDelegate)] || ![vc2 conformsToProtocol:@protocol(QMUINavigationControllerDelegate)]) {
return NO;// 只处理前后两个界面都是 QMUI 系列的场景
}
//2.
if ([vc1 respondsToSelector:@selector(customNavigationBarTransitionKey)] || [vc2 respondsToSelector:@selector(customNavigationBarTransitionKey)]) {
NSString *key1 = [vc1 respondsToSelector:@selector(customNavigationBarTransitionKey)] ? [vc1 customNavigationBarTransitionKey] : nil;
NSString *key2 = [vc2 respondsToSelector:@selector(customNavigationBarTransitionKey)] ? [vc2 customNavigationBarTransitionKey] : nil;
BOOL result = (key1 || key2) && ![key1 isEqualToString:key2];
return result;
}
//3
if (!AutomaticCustomNavigationBarTransitionStyle) {
return NO;
}
//4
UIImage *bg1 = [vc1 respondsToSelector:@selector(navigationBarBackgroundImage)] ? [vc1 navigationBarBackgroundImage] : [[UINavigationBar appearance] backgroundImageForBarMetrics:UIBarMetricsDefault];
UIImage *bg2 = [vc2 respondsToSelector:@selector(navigationBarBackgroundImage)] ? [vc2 navigationBarBackgroundImage] : [[UINavigationBar appearance] backgroundImageForBarMetrics:UIBarMetricsDefault];
if (bg1 || bg2) {
if (!bg1 || !bg2) {
return YES;// 一个有一个没有,则需要自定义
}
if (![bg1.qmui_averageColor isEqual:bg2.qmui_averageColor]) {
return YES;// 目前只能判断图片颜色是否相等了
}
}
// 如果存在 backgroundImage,则 barTintColor、barStyle 就算存在也不会被显示出来,所以这里只判断两个 backgroundImage 都不存在的时候
if (!bg1 && !bg2) {
UIColor *barTintColor1 = [vc1 respondsToSelector:@selector(navigationBarBarTintColor)] ? [vc1 navigationBarBarTintColor] : [UINavigationBar appearance].barTintColor;
UIColor *barTintColor2 = [vc2 respondsToSelector:@selector(navigationBarBarTintColor)] ? [vc2 navigationBarBarTintColor] : [UINavigationBar appearance].barTintColor;
if (barTintColor1 || barTintColor2) {
if (!barTintColor1 || !barTintColor2) {
return YES;
}
if (![barTintColor1 isEqual:barTintColor2]) {
return YES;
}
}
UIBarStyle barStyle1 = [vc1 respondsToSelector:@selector(navigationBarStyle)] ? [vc1 navigationBarStyle] : [UINavigationBar appearance].barStyle;
UIBarStyle barStyle2 = [vc2 respondsToSelector:@selector(navigationBarStyle)] ? [vc2 navigationBarStyle] : [UINavigationBar appearance].barStyle;
if (barStyle1 != barStyle2) {
return YES;
}
}
UIImage *shadowImage1 = [vc1 respondsToSelector:@selector(navigationBarShadowImage)] ? [vc1 navigationBarShadowImage] : (vc1.navigationController.navigationBar ? vc1.navigationController.navigationBar.shadowImage : (QMUICMIActivated ? NavBarShadowImage : nil));
UIImage *shadowImage2 = [vc2 respondsToSelector:@selector(navigationBarShadowImage)] ? [vc2 navigationBarShadowImage] : (vc2.navigationController.navigationBar ? vc2.navigationController.navigationBar.shadowImage : (QMUICMIActivated ? NavBarShadowImage : nil));
if (shadowImage1 || shadowImage2) {
if (!shadowImage1 || !shadowImage2) {
return YES;
}
if (![shadowImage1.qmui_averageColor isEqual:shadowImage2.qmui_averageColor]) {
return YES;
}
}
return NO;
}
QMUI分别通过UIViewController和UINavigationController的分类通过runtime的方式扩展其方法。
以UIViewController的分类为例(UIViewController +NavigationBarTransition)
分别重载了:viewWillAppear:、viewDidAppear:、viewDidDisappear、viewWillLayoutSubviews这四个声明周期的方法。
以A push B为例,根据图1
- 第一步B的viewWillAppear,渲染导航栏背景色。这里Controller遵循QMUINavigationControllerAppearanceDelegate协议。因为每个Controller的navigationbar的样式都是通过该协议去设置。
- 第二步B的layoutSubviews:根据上面的判断方法,判断是否添加fake。
if (isCurrentToViewController && !selfObject.lockTransitionNavigationBar) {
BOOL shouldCustomNavigationBarTransition = NO;
//如果没有fakebar
if (!selfObject.transitionNavigationBar) {
//判断是否需要fakebar
if ([selfObject shouldCustomTransitionAutomaticallyWithFirstViewController:fromViewController secondViewController:toViewController]) {
shouldCustomNavigationBarTransition = YES;
}
if (shouldCustomNavigationBarTransition) {
if (selfObject.navigationController.navigationBar.translucent) {
// 如果原生bar是半透明的,需要给containerView加个背景色,否则有可能会看到下面的默认黑色背景色
toViewController.originContainerViewBackgroundColor = [transitionCoordinator containerView].backgroundColor;
[transitionCoordinator containerView].backgroundColor = [selfObject containerViewBackgroundColor];
}
//添加fakebar到Controller的view中
[selfObject addTransitionNavigationBarIfNeeded];
//设置fakebar的frame
[selfObject resizeTransitionNavigationBarFrame];
selfObject.navigationController.navigationBar.transitionNavigationBar = selfObject.transitionNavigationBar;
//隐藏系统的bar
selfObject.prefersNavigationBarBackgroundViewHidden = YES;
}
}
}
- (void)addTransitionNavigationBarIfNeeded {
if (!self.view.qmui_visible || !self.navigationController.navigationBar) {
return;
}
//把原来的样式赋值给fakebar
UINavigationBar *originBar = self.navigationController.navigationBar;
_QMUITransitionNavigationBar *customBar = [[_QMUITransitionNavigationBar alloc] init];
if (customBar.barStyle != originBar.barStyle) {
customBar.barStyle = originBar.barStyle;
}
if (customBar.translucent != originBar.translucent) {
customBar.translucent = originBar.translucent;
}
if (![customBar.barTintColor isEqual:originBar.barTintColor]) {
customBar.barTintColor = originBar.barTintColor;
}
UIImage *backgroundImage = [originBar backgroundImageForBarMetrics:UIBarMetricsDefault];
if (backgroundImage && backgroundImage.size.width <= 0 && backgroundImage.size.height <= 0) {
// 假设这里的图片时通过`[UIImage new]`这种形式创建的,那么会navBar会奇怪地显示为系统默认navBar的样式。不知道为什么 navController 设置自己的 navBar 为 [UIImage new] 却没事,所以这里做个保护。
backgroundImage = [UIImage qmui_imageWithColor:UIColorClear];
}
[customBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault];
[customBar setShadowImage:originBar.shadowImage];
self.transitionNavigationBar = customBar;
[self resizeTransitionNavigationBarFrame];
if (!self.navigationController.navigationBarHidden) {
[self.view addSubview:self.transitionNavigationBar];
}
CGRect viewRect = [self.navigationController.view convertRect:self.view.frame fromView:self.view.superview];
if (viewRect.origin.y != 0 && self.view.clipsToBounds) {
QMUILog(@"UINavigationController+NavigationBarTransition", @"⚠️⚠️⚠️注意啦:当前界面 controller.view = %@ 布局并没有从屏幕顶部开始,可能会导致自定义导航栏转场的假 bar 看不到", self);
}
}
- 如果push过程中布局没有改变,那么不会调用虚线的部分
- A的viewDidDisappear: A控制器调用父类方法。
- B的viewDidAppear: 重新设置navigationbar并删除fakebar
selfObject.lockTransitionNavigationBar = YES;
f (selfObject.transitionNavigationBar) {
[UIViewController replaceStyleForNavigationBar:selfObject.transitionNavigationBar withNavigationBar:selfObject.navigationController.navigationBar];
[selfObject removeTransitionNavigationBar];
id <UIViewControllerTransitionCoordinator> transitionCoordinator = selfObject.transitionCoordinator;
[transitionCoordinator containerView].backgroundColor = selfObject.originContainerViewBackgroundColor;
}
if ([selfObject.navigationController.viewControllers containsObject:selfObject]) {
// 防止一些 childViewController 走到这里
selfObject.prefersNavigationBarBackgroundViewHidden = NO;
}
基本的流程大致是这样
参考:美团导航栏解决