引
如我在传送门:iOS导航栏切换界面时隐藏和显示中所说,现在很多App的个人中心模块都是不保留导航栏的,会直接使导航栏透明,比如做的很好的QQ个人信息界面:
为什么说QQ做的很好呢?既然有透明的导航栏也有不透明的导航栏,那一定会在界面切换之间存在一个过渡的过程,而这个过程,QQ做的特别好,在从透明导航栏界面返回到不透明导航栏界面时,导航栏的透明度是一个渐进的过渡效果,甚至会有一种毛玻璃的效果,感兴趣的可以打开手机QQ到个人界面看一看,效果很赞。
而很多App的做法其实比较粗糙,类似于我在传送门:iOS导航栏切换界面时隐藏和显示中的做法,需要导航栏透明时,直接将导航栏隐藏起来。直接隐藏起来的意思是,整个导航栏就用不了了,也就是说,标题、返回按钮等都需要自己去做,这是一个比较麻烦的地方,此外,在有无导航栏的界面间切换时,过程是比较生硬的,导航栏不是渐变出现的。如果说这些都可以接受,那最大的一个问题,也是我在那篇文章里提到的,如果正好处于用UITabbarConatroller切换界面,那么导航栏会有一个往上缩回的快速动画,这其实就很不美观了,当然我们可以通过将隐藏导航栏的动画去掉来达到对Tabbar切换友好的效果:
[self.navigationController setNavigationBarHidden:NO animated:NO];
但是这样一来你在UINavigationController体系下切换界面时由于没有了动画,这边的效果又会变得很差。这两个矛盾没有想到可以调和的手段,除非在业务上就不显示Tabbar了,但始终不是长久之计。
同时,我们虽然说QQ做的很好,但也依然有一些不足,多把玩一下导航栏过渡的过程就会发现,如果准备从透明导航栏返回时又决定不反回了,还是停留在导航栏透明的界面,这时候导航栏虽然会回到透明,但会有一个导航栏闪现一下的小瑕疵。
现在问题已经讲完了,基于这些问题,我们自己来尝试实现一种更好的平滑过渡效果,不自定义导航栏,直接利用系统原生的导航栏,使用Category和Runtime的技术,达到这个效果:
代码可以在示例工程下载(觉得有帮助的小伙伴请不吝加Star~):https://github.com/Cloudox/SmoothNavDemo
实现过程
其实我们的目的总结起来有三个:
1、不去自定义导航栏,就用系统原生的,标题、返回按钮啥的都方便加,这也就是说不隐藏导航栏,而是要单独让导航栏背景透明;
2、在导航栏透明与否的界面间切换时透明度有渐变效果;
3、在UINavigationController体系和UITabarController体系下切换界面都很完美。
对于第三个目的,我们之前在UITabarController下切换时会有导航栏隐藏的小动画,但如果我们满足了第一个目的,那就不存在隐藏导航栏了,所以第三个问题也就不会存在了。
我们先来看第一个目的。
设置导航栏背景透明度
导航栏上应该是有很多view的,我们要做的是只让背景透明,而保留标题、返回按钮。iOS没有直接给我们提供对于导航栏背景view的访问途径,那么我们只能自己来找了。
首先我们遍历打印出UINavigationBar的所有子视图,是所有,包括子视图的一层层子视图,来看看到底导航栏都包含了哪些东西:
上面这张图就是导航栏UINavigationBar所包含的所有子view了,序号和缩进表示了其层级归属关系,打印的方法可以看这篇文章:传送门:iOS遍历打印所有子视图
从这些子view的类名能够大概猜出他们都是导航栏上的什么,让我们大胆猜测一下,_UIBarBackground 是背景视图,下属的 UIImageView 是背景图片,_UINavigationBarBackIndicatorView 是返回箭头,UINavigationItemView 是添加的一些导航栏按钮,包括返回按钮,因为我没有给导航栏添加任何其他按钮,所以这里一定是返回按钮,下属的 UILabel 就是 “返回” 两个字了。
根据上面得到的信息,我们就尝试将_UIBarBackground、UIImageView、UIVisualEffectView的 alpha 值设为 1 或者 0 来改变导航栏背景的透明度。
我们可以给 UINavigationController 创建一个类别,来给这个类添加一个方法,用于设置导航栏的透明度:
// UIViewController+Cloudox.m
- (void)setNeedsNavigationBackground:(CGFloat)alpha {
// 导航栏背景透明度设置
UIView *barBackgroundView = [[self.navigationBar subviews] objectAtIndex:0];// _UIBarBackground
UIImageView *backgroundImageView = [[barBackgroundView subviews] objectAtIndex:0];// UIImageView
if (self.navigationBar.isTranslucent) {
if (backgroundImageView != nil && backgroundImageView.image != nil) {
barBackgroundView.alpha = alpha;
} else {
UIView *backgroundEffectView = [[barBackgroundView subviews] objectAtIndex:1];// UIVisualEffectView
if (backgroundEffectView != nil) {
backgroundEffectView.alpha = alpha;
}
}
} else {
barBackgroundView.alpha = alpha;
}
}
到目前为止,我们会得到什么效果呢?看一下:
我们成功的将导航栏背景设为透明了!但是那条细线是什么情况?!有它在岂不是前功尽弃了,再用上面的方法已经不管用了,这条线不在我们找出来的子view之中,通过查资料,要隐藏这跟细线的方法很多,但是要跟我们对导航栏背景的设置不冲突,又要能到只在将导航栏背景设为透明时才隐藏,下面这种方法是比较好的方法:
// 对导航栏下面那条线做处理
self.navigationBar.clipsToBounds = alpha == 0.0;
当我们对导航栏的透明度设为 0 时,就会隐藏细线,否则不隐藏,这样当切换到其他界面时,细线就又会出来了。
现在导航栏的透明就比较完美了:
对于这种将导航栏背景直接设为透明的情况,在 Tabbar 切换界面时,也不会出现导航栏收起的小动画:
为UIViewController添加导航栏透明度属性
为了方便,我们创建一个 UIViewController 的Category,为其增加一个属性——导航栏透明度(navBarBgAlpha),Category一般是不可以添加属性的,但我们可以通过Runtime的关联对象来做到,具体做法参看我的这篇文章:传送门:iOS中OC给Category添加属性,由于只能关联对象,所以我们无法直接添加 CGFloat 类型的属性,我们就直接添加 NSString 类型的属性就好了,用的时候再用 [NSString floatValue] 方法。这样每个 ViewController 都可以管理自己的导航栏透明度,在这个新增属性的setter方法中,我们调用前面在在 UINavigationController 的Category 中添加的设置导航栏透明度的方法,这样就打通了。
UIViewController的设置方法如下:
// UIViewController+Cloudox.h
@interface UIViewController (Cloudox)
@property (copy, nonatomic) NSString *navBarBgAlpha;
@end
// UIViewController+Cloudox.m
#import "UIViewController+Cloudox.h"
// 导入runtime才可以使用关联对象
#import <objc/runtime.h>
// 导入我们的Category才可以调用我们添加的方法
#import "UINavigationController+Cloudox.h"
@implementation UIViewController (Cloudox)
//定义常量 必须是C语言字符串
static char *CloudoxKey = "CloudoxKey";
-(void)setNavBarBgAlpha:(NSString *)navBarBgAlpha{
/*
OBJC_ASSOCIATION_ASSIGN; //assign策略
OBJC_ASSOCIATION_COPY_NONATOMIC; //copy策略
OBJC_ASSOCIATION_RETAIN_NONATOMIC; // retain策略
OBJC_ASSOCIATION_RETAIN;
OBJC_ASSOCIATION_COPY;
*/
/*
* id object 给哪个对象的属性赋值
const void *key 属性对应的key
id value 设置属性值为value
objc_AssociationPolicy policy 使用的策略,是一个枚举值,和copy,retain,assign是一样的,手机开发一般都选择NONATOMIC
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
*/
objc_setAssociatedObject(self, CloudoxKey, navBarBgAlpha, OBJC_ASSOCIATION_COPY_NONATOMIC);
// 设置导航栏透明度(利用Category自己添加的方法)
[self.navigationController setNeedsNavigationBackground:[navBarBgAlpha floatValue]];
}
-(NSString *)navBarBgAlpha{
return objc_getAssociatedObject(self, CloudoxKey);
}
@end
使用时我们只需要:
// 让导航栏透明
self.navBarBgAlpha = @"0.0";
// 让导航栏不透明
self.navBarBgAlpha = @"1.0";
实现切换界面时渐变过渡
现在实现了比较好的透明导航栏效果,但在透明的导航栏与不透明的导航栏界面直接切换时,导航栏的透明度是直接跳变的:
而我们想要的是像QQ一样从完全透明到不透明之间有一个随着滑动手势变化的透明度渐变效果,这样是最好的转场效果了。
我们需要的随着手势滑动返回界面的进度,来实时变化导航栏的透明度,比如滑动到了界面一半的时候,导航栏透明度应该是 0.5。对于这个需求,首先想到的是,我们要监控这个滑动事件的滑动进度。
正好,UINavigationController 有一个方法 _updateInteractiveTransition: 就是监控这个手势及其进度的,那么我们就可以使用 Runtime 黑魔法——方法交换来实现我们的需求。
怎么交换呢?通过要交换的方法和我们定义的方法的名称,获取到对应的方法实现,然后用 method_exchangeImplementations 方法交换两个方法的实现:
+ (void)initialize {
if (self == [UINavigationController self]) {
// 交换方法
SEL originalSelector = NSSelectorFromString(@"_updateInteractiveTransition:");
SEL swizzledSelector = NSSelectorFromString(@"et__updateInteractiveTransition:");
Method originalMethod = class_getInstanceMethod([self class], originalSelector);
Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
这一步我们在 initialize 方法中去做,这样一调用时就会生效了,关于 initialize 可以查看这篇文章:传送门:OC中load方法和initialize方法的异同。
我们自己创建一个用于交换的方法,这个方法中,除了调用原方法外(注意由于方法名称对应的实现已经交换了,这里我们目的是调用原实现,但是使用的名称确实本方法自己的名称),还添加一个处理,_updateInteractiveTransition: 有一个参数就是界面滑动过程的百分比,那么我们获取上一个界面的导航栏透明度、下一个界面的导航栏透明度、以及滑动的进度,通过很简单的数学计算就可以得出当前进度应该对应的透明度是多少了,这里也可以看出我们给 ViewController 添加一个导航栏透明度属性是多么有意义,这里就可以直接调用了,当然,要记得导入我们的Category:
// 交换的方法,监控滑动手势
- (void)et__updateInteractiveTransition:(CGFloat)percentComplete {
[self et__updateInteractiveTransition:(percentComplete)];
UIViewController *topVC = self.topViewController;
if (topVC != nil) {
id<UIViewControllerTransitionCoordinator> coor = topVC.transitionCoordinator;
if (coor != nil) {
// 随着滑动的过程设置导航栏透明度渐变
CGFloat fromAlpha = [[coor viewControllerForKey:UITransitionContextFromViewControllerKey].navBarBgAlpha floatValue];
CGFloat toAlpha = [[coor viewControllerForKey:UITransitionContextToViewControllerKey].navBarBgAlpha floatValue];
CGFloat nowAlpha = fromAlpha + (toAlpha - fromAlpha) * percentComplete;
NSLog(@"from:%f, to:%f, now:%f",fromAlpha, toAlpha, nowAlpha);
[self setNeedsNavigationBackground:nowAlpha];
}
}
}
我们打印了透明度渐变的过程,可以看一下:
是按照预想地在随着滑动界面的进度渐变透明度的,实际的效果也是这样的:
一些小瑕疵的修补
就目前的效果,其实还是不错的,不过也有一些小瑕疵,比如滑动到一半松手时会有一个小跳变,对于这一点,我们可以在 UINavigationController 的 Delegate 中添加一个处理,监控松手后时自动完成返回还是取消返回操作,同时使用 UIView 动画(关于 UIView 动画可以看我的这篇文章:传送门:iOS基础动画教程),在自动操作的那个时间内将透明度变为对应界面的导航栏透明度,让其变化的不那么跳跃:
#pragma mark - UINavigationController Delegate
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
UIViewController *topVC = self.topViewController;
if (topVC != nil) {
id<UIViewControllerTransitionCoordinator> coor = topVC.transitionCoordinator;
if (coor != nil) {
[coor notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context){
[self dealInteractionChanges:context];
}];
}
}
}
- (void)dealInteractionChanges:(id<UIViewControllerTransitionCoordinatorContext>)context {
if ([context isCancelled]) {// 自动取消了返回手势
NSTimeInterval cancelDuration = [context transitionDuration] * (double)[context percentComplete];
[UIView animateWithDuration:cancelDuration animations:^{
CGFloat nowAlpha = [[context viewControllerForKey:UITransitionContextFromViewControllerKey].navBarBgAlpha floatValue];
NSLog(@"自动取消返回到alpha:%f", nowAlpha);
[self setNeedsNavigationBackground:nowAlpha];
}];
} else {// 自动完成了返回手势
NSTimeInterval finishDuration = [context transitionDuration] * (double)(1 - [context percentComplete]);
[UIView animateWithDuration:finishDuration animations:^{
CGFloat nowAlpha = [[context viewControllerForKey:
UITransitionContextToViewControllerKey].navBarBgAlpha floatValue];
NSLog(@"自动完成返回到alpha:%f", nowAlpha);
[self setNeedsNavigationBackground:nowAlpha];
}];
}
}
对于直接点击返回按钮以及 push 到下一个界面的操作,也可以增加一次处理:
#pragma mark - UINavigationBar Delegate
- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item {
if (self.viewControllers.count >= navigationBar.items.count) {// 点击返回按钮
UIViewController *popToVC = self.viewControllers[self.viewControllers.count - 1];
[self setNeedsNavigationBackground:[popToVC.navBarBgAlpha floatValue]];
}
}
- (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item {
// push到一个新界面
[self setNeedsNavigationBackground:[self.topViewController.navBarBgAlpha floatValue]];
}
不过意义不是特别大。
结
以上这些处理基本都在 Category 里写代码,一次搞定,真正在自己的 ViewController 需要做的只是一句:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.navBarBgAlpha = @"0.0";
}
很简单吧~更多效果有兴趣的可以自己继续修修补补,这个过程也是很有意思的。
再次宣传,代码可以在示例工程下载(觉得有帮助的小伙伴请不吝加Star~):https://github.com/Cloudox/SmoothNavDemo
参考(swift):http://www.jianshu.com/p/454b06590cf1
关注我的公众号【月亮与二进制】,鹅厂程序员的敲码间隙,也能读书观影练剑写字,分享给你我的世界