神乎其技的导航栏透明度渐变
一、写在前面
好久没有更新文章了,因为自身时间安排的原因,而且当你着手写的时候才发现,要把一系列不那么简单的逻辑用文字表述明白,真的很难。就这篇文章浮夸的标题,其实我就想了大概2分钟😖~
二、屌丝的自定义导航栏实现
在项目中,很多时候某些页面导航栏是不显示的,并且类似于个人信息页面,滑动过程中导航栏还会随之显示或隐藏。大多数童鞋都是这样处理的:
- viewWillAppear:方法中设置导航栏隐藏
- viewWillDisappear:方法中设置导航栏显示
- 重写一个与系统导航栏等高的view,操作其alpha值
一切都是那么的完美,舒舒服服~
三、情景再现
直到某一天,产品大大心血来潮,新需求应声而到:你们iOS系统不是支持侧滑的吗?为什么咱们的App不能侧滑,赶紧加上,今天发个新包
对于一个看似简单的需求,很多屌丝会立马说一句:so easy~,于是咔咔咔一通code。打包测试的时候,测试妹纸会很快发现下面这样的一个bug
bug2
总之很屌丝
以上情景纯属虚构,如若雷同,必是巧合
四、分析bug产生原因
比较简单,大伙儿结合上面处理的方式进行分析,就能明白了。
五、问题解决思路
方案一
把导航控制器的根控制器导航栏隐藏,使用自定义view作为导航栏。这样侧滑返回的时候,就不会有在viewWillAppear与viewWillDisAppear中操作系统导航栏是否隐藏的逻辑。从而避免了上面两种bug的产生。
缺点: 太过于屌丝,为了解决这一个bug,需要将根控制器到有此需求的控制器之间的所有导航栏都隐藏,并针对每个页面画伪导航栏。并且完全舍弃了系统侧滑时导航栏动画,效果僵硬。
方案二
最近的一篇文章也有介绍过自定义转场动画的实现。我们在系统SDK提供的转场协议方法中,针对fromVC与toVC中的控件,做自定义的转场动画实现。
其实导航侧滑返回也是系统的转场的一种。但是对于同一个导航控制器下的视图控制器,导航栏透明度属性都是全局的,并不属于fromVC与toVC的任何一个。所以首先要做的就是针对试图控制器,添加一个导航栏透明度属性。
1. 通过VC的alpha,控制导航栏透明度
在runtime实现setter方法时,我们背地里实际上是操作视图控制器所在的导航控制器的导航栏的透明度。代码如下:
- (void)setNavAlpha:(CGFloat)navAlpha
{
objc_setAssociatedObject(self, @selector(navAlpha), @(navAlpha), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self.navigationController setNavigationBackgroundAlpha:navAlpha];
}
- (void)setNavigationBackgroundAlpha:(CGFloat)alpha
{
//1.找到导航bar上第一个背景视图
UIView *barView = [[self.navigationBar subviews] firstObject];
//2.kvc获取阴影视图
UIView *shadowView = [barView valueForKey:@"_shadowView"];
//3.如果能够获取到设置透明度
if (shadowView) {
shadowView.alpha = alpha;
}
//4.如果导航栏默认没有设置半透明,背景视图透明度也进行改变
if (!self.navigationBar.isTranslucent) {
barView.alpha = alpha;
return;
}
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0) {
UIView *backEffectView = [barView valueForKey:@"_backgroundEffectView"];
if (backEffectView && [self.navigationBar backgroundImageForBarMetrics:UIBarMetricsDefault] == nil) {
backEffectView.alpha = alpha;
}
} else {
UIView *daptiveBackdrop = [barView valueForKey:@"_adaptiveBackdrop"];
UIView *backdropEffectView = [daptiveBackdrop valueForKey:@"_backdropEffectView"];
if (daptiveBackdrop != nil && backdropEffectView != nil ) {
backdropEffectView.alpha = alpha;
}
}
}
2.手动Pop,动态改变导航栏透明度
重写UINavigationController的协议方法,在pop前,偷偷地操作导航栏。
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
UIViewController *topVC = self.topViewController;
id <UIViewControllerTransitionCoordinator> transitionCtx = topVC.transitionCoordinator;
if (topVC && transitionCtx && transitionCtx.initiallyInteractive) {
if ([[UIDevice currentDevice].systemVersion floatValue]>=10.0) {
[transitionCtx notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
//因为侧滑返回也会执行此协议方法,所以在这里处理手势专场取消的情况
[self dealInteractionChanges:context];
}];
} else {
[transitionCtx notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
//因为侧滑返回也会执行此协议方法,所以在这里处理手势专场取消的情况
[self dealInteractionChanges:context];
}];
}
return YES;
}
UIViewController *popToVc = self.viewControllers[self.viewControllers.count - 2];
[self popToViewController:popToVc animated:YES];
return YES;
}
- (void)dealInteractionChanges:(id <UIViewControllerTransitionCoordinatorContext>)context
{
void(^animations)(NSString *) = ^(NSString *key) {
CGFloat nowAlpha = [context viewControllerForKey:key].navAlpha;
[self setNavigationBackgroundAlpha:nowAlpha];
self.navigationBar.tintColor = [context viewControllerForKey:key].navTintColor;
// self.navigationBar.barTintColor = [context viewControllerForKey:key].navBarTintColor;
};
if (context.isCancelled) {
//拖动取消,使用toVC的属性相关
NSTimeInterval cancaleDuration = context.transitionDuration * context.percentComplete;
[UIView animateWithDuration:cancaleDuration animations:^{
animations(UITransitionContextFromViewControllerKey);
}];
} else {
//正常拖动,使用fromVC的属性相关
NSTimeInterval finishDuration = context.transitionDuration * (1 - context.percentComplete);
[UIView animateWithDuration:finishDuration animations:^{
animations(UITransitionContextToViewControllerKey);
}];
}
}
因为侧滑返回也会执行此协议方法,而侧滑返回不同于手动返回的一点就是,侧滑返回中途有可能cancel。所以上面的方法根据转场上下文协议是否cancel。来确定最后使用fromVC的alpha还是toVC的alpha
3.侧滑Pop,动态改变导航栏透明度
根据上面两点的实现,效果如下:
目前的效果
可以看到,手动侧滑的过程中,缺少了渐变的效果。在之前的自定义转场动画中,我们知道转场过程中,有一个方法会持续执行。
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
使用runtime对此方法进行方法交换,在我们交换的方法中,根据进度percentComplete对导航栏alpha做动态改变处理:
- (void)xll_updateInteractiveTransition:(CGFloat)percentComplete
{
UIViewController *topVC = self.topViewController;
if (!topVC) return;
//1.获取转场上下文协议
id <UIViewControllerTransitionCoordinator>transitionCtx = topVC.transitionCoordinator;
//2.根据转场上下文协议获取转场始末控制器
UIViewController *fromVC = [transitionCtx viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionCtx viewControllerForKey:UITransitionContextToViewControllerKey];
//3.获取始末控制器导航栏透明度
CGFloat fromAlpha = fromVC.navAlpha;
CGFloat toAlpha = toVC.navAlpha;
//4.计算出转场过程中变化的透明度值
CGFloat newAlpha = fromAlpha+(toAlpha-fromAlpha)*percentComplete;
//5.重新设定透明度
[self setNavigationBackgroundAlpha:newAlpha];
[self xll_updateInteractiveTransition:percentComplete];
}
效果如下:
最终效果图
六、后期思考
导航栏上不仅有导航栏透明度,还有导航栏背景颜色,导航标题大小与颜色,导航左右item内容颜色,状态栏样式等。这些都有可能在相邻的两个页面不同。
文章也是匆忙写的,讲述的也比较笼统,小伙伴们结合Demo会更有效地明白我要表达的意思这是Demo,后期也会慢慢将以上所考虑到的实现代码加进去。希望发现问题及时指正,共同进步🙂