引
如我在傳送門:iOS導航欄切換界面時隱藏和顯示中所說,現(xiàn)在很多App的個人中心模塊都是不保留導航欄的龄章,會直接使導航欄透明做裙,比如做的很好的QQ個人信息界面:
為什么說QQ做的很好呢仔戈?既然有透明的導航欄也有不透明的導航欄拧廊,那一定會在界面切換之間存在一個過渡的過程卦绣,而這個過程滤港,QQ做的特別好溅漾,在從透明導航欄界面返回到不透明導航欄界面時添履,導航欄的透明度是一個漸進的過渡效果暮胧,甚至會有一種毛玻璃的效果,感興趣的可以打開手機QQ到個人界面看一看钞翔,效果很贊严卖。
而很多App的做法其實比較粗糙,類似于我在傳送門:iOS導航欄切換界面時隱藏和顯示中的做法布轿,需要導航欄透明時哮笆,直接將導航欄隱藏起來。直接隱藏起來的意思是汰扭,整個導航欄就用不了了稠肘,也就是說,標題萝毛、返回按鈕等都需要自己去做启具,這是一個比較麻煩的地方珊泳,此外薯演,在有無導航欄的界面間切換時,過程是比較生硬的衡创,導航欄不是漸變出現(xiàn)的。如果說這些都可以接受一也,那最大的一個問題,也是我在那篇文章里提到的,如果正好處于用UITabbarConatroller切換界面层皱,那么導航欄會有一個往上縮回的快速動畫,這其實就很不美觀了,當然我們可以通過將隱藏導航欄的動畫去掉來達到對Tabbar切換友好的效果:
[self.navigationController setNavigationBarHidden:NO animated:NO];
但是這樣一來你在UINavigationController體系下切換界面時由于沒有了動畫方淤,這邊的效果又會變得很差。這兩個矛盾沒有想到可以調(diào)和的手段,除非在業(yè)務上就不顯示Tabbar了鸳谜,但始終不是長久之計。
同時蝗肪,我們雖然說QQ做的很好俺陋,但也依然有一些不足术浪,多把玩一下導航欄過渡的過程就會發(fā)現(xiàn),如果準備從透明導航欄返回時又決定不反回了硕并,還是停留在導航欄透明的界面埃仪,這時候?qū)Ш綑陔m然會回到透明,但會有一個導航欄閃現(xiàn)一下的小瑕疵傻丝。
現(xiàn)在問題已經(jīng)講完了忱反,基于這些問題,我們自己來嘗試實現(xiàn)一種更好的平滑過渡效果韭畸,不自定義導航欄,直接利用系統(tǒng)原生的導航欄锦庸,使用Category和Runtime的技術(shù)梆掸,達到這個效果:
代碼可以在示例工程下載(覺得有幫助的小伙伴請不吝加Star~):https://github.com/Cloudox/SmoothNavDemo
實現(xiàn)過程
其實我們的目的總結(jié)起來有三個:
1、不去自定義導航欄蚕断,就用系統(tǒng)原生的亿乳,標題匠璧、返回按鈕啥的都方便加,這也就是說不隱藏導航欄酿雪,而是要單獨讓導航欄背景透明州丹;
2、在導航欄透明與否的界面間切換時透明度有漸變效果柠辞;
3、在UINavigationController體系和UITabarController體系下切換界面都很完美。
對于第三個目的,我們之前在UITabarController下切換時會有導航欄隱藏的小動畫荣瑟,但如果我們滿足了第一個目的嚷掠,那就不存在隱藏導航欄了熊楼,所以第三個問題也就不會存在了。
我們先來看第一個目的。
設置導航欄背景透明度
導航欄上應該是有很多view的计济,我們要做的是只讓背景透明凑队,而保留標題遗增、返回按鈕抡草。iOS沒有直接給我們提供對于導航欄背景view的訪問途徑腿短,那么我們只能自己來找了钝诚。
首先我們遍歷打印出UINavigationBar的所有子視圖祈噪,是所有杠茬,包括子視圖的一層層子視圖舀透,來看看到底導航欄都包含了哪些東西:
上面這張圖就是導航欄UINavigationBar所包含的所有子view了坠狡,序號和縮進表示了其層級歸屬關系婴渡,打印的方法可以看這篇文章:傳送門:iOS遍歷打印所有子視圖
從這些子view的類名能夠大概猜出他們都是導航欄上的什么硼瓣,讓我們大膽猜測一下瘟栖,_UIBarBackground 是背景視圖签餐,下屬的 UIImageView 是背景圖片冠摄,_UINavigationBarBackIndicatorView 是返回箭頭,UINavigationItemView 是添加的一些導航欄按鈕薄霜,包括返回按鈕惰瓜,因為我沒有給導航欄添加任何其他按鈕负甸,所以這里一定是返回按鈕奏篙,下屬的 UILabel 就是 “返回” 兩個字了第股。
根據(jù)上面得到的信息涉馅,我們就嘗試將_UIBarBackground偶翅、UIImageView聚谁、UIVisualEffectView的 alpha 值設為 1 或者 0 來改變導航欄背景的透明度炫隶。
我們可以給 UINavigationController 創(chuàng)建一個類別,來給這個類添加一個方法斟湃,用于設置導航欄的透明度:
// 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;
}
}
到目前為止陶衅,我們會得到什么效果呢勇皇?看一下:
我們成功的將導航欄背景設為透明了慨丐!但是那條細線是什么情況咧纠?!有它在豈不是前功盡棄了瓶埋,再用上面的方法已經(jīng)不管用了晕粪,這條線不在我們找出來的子view之中阅嘶,通過查資料,要隱藏這跟細線的方法很多,但是要跟我們對導航欄背景的設置不沖突冲粤,又要能到只在將導航欄背景設為透明時才隱藏,下面這種方法是比較好的方法:
// 對導航欄下面那條線做處理
self.navigationBar.clipsToBounds = alpha == 0.0;
當我們對導航欄的透明度設為 0 時碌奉,就會隱藏細線咐汞,否則不隱藏墙贱,這樣當切換到其他界面時纤房,細線就又會出來了。
現(xiàn)在導航欄的透明就比較完美了:
對于這種將導航欄背景直接設為透明的情況俄认,在 Tabbar 切換界面時糊探,也不會出現(xiàn)導航欄收起的小動畫:
為UIViewController添加導航欄透明度屬性
為了方便部念,我們創(chuàng)建一個 UIViewController 的Category,為其增加一個屬性——導航欄透明度(navBarBgAlpha)屯烦,Category一般是不可以添加屬性的露懒,但我們可以通過Runtime的關聯(lián)對象來做到,具體做法參看我的這篇文章:傳送門:iOS中OC給Category添加屬性砂心,由于只能關聯(lián)對象懈词,所以我們無法直接添加 CGFloat 類型的屬性,我們就直接添加 NSString 類型的屬性就好了辩诞,用的時候再用 [NSString floatValue] 方法坎弯。這樣每個 ViewController 都可以管理自己的導航欄透明度,在這個新增屬性的setter方法中译暂,我們調(diào)用前面在在 UINavigationController 的Category 中添加的設置導航欄透明度的方法抠忘,這樣就打通了。
UIViewController的設置方法如下:
// UIViewController+Cloudox.h
@interface UIViewController (Cloudox)
@property (copy, nonatomic) NSString *navBarBgAlpha;
@end
// UIViewController+Cloudox.m
#import "UIViewController+Cloudox.h"
// 導入runtime才可以使用關聯(lián)對象
#import <objc/runtime.h>
// 導入我們的Category才可以調(diào)用我們添加的方法
#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是一樣的囚灼,手機開發(fā)一般都選擇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";
實現(xiàn)切換界面時漸變過渡
現(xiàn)在實現(xiàn)了比較好的透明導航欄效果,但在透明的導航欄與不透明的導航欄界面直接切換時祭衩,導航欄的透明度是直接跳變的:
而我們想要的是像QQ一樣從完全透明到不透明之間有一個隨著滑動手勢變化的透明度漸變效果灶体,這樣是最好的轉(zhuǎn)場效果了。
我們需要的隨著手勢滑動返回界面的進度汪厨,來實時變化導航欄的透明度赃春,比如滑動到了界面一半的時候,導航欄透明度應該是 0.5劫乱。對于這個需求织中,首先想到的是,我們要監(jiān)控這個滑動事件的滑動進度衷戈。
正好狭吼,UINavigationController 有一個方法 _updateInteractiveTransition: 就是監(jiān)控這個手勢及其進度的,那么我們就可以使用 Runtime 黑魔法——方法交換來實現(xiàn)我們的需求殖妇。
怎么交換呢刁笙?通過要交換的方法和我們定義的方法的名稱,獲取到對應的方法實現(xiàn),然后用 method_exchangeImplementations 方法交換兩個方法的實現(xiàn):
+ (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 方法中去做疲吸,這樣一調(diào)用時就會生效了座每,關于 initialize 可以查看這篇文章:傳送門:OC中l(wèi)oad方法和initialize方法的異同。
我們自己創(chuàng)建一個用于交換的方法摘悴,這個方法中峭梳,除了調(diào)用原方法外(注意由于方法名稱對應的實現(xiàn)已經(jīng)交換了,這里我們目的是調(diào)用原實現(xiàn)蹂喻,但是使用的名稱確實本方法自己的名稱)葱椭,還添加一個處理,_updateInteractiveTransition: 有一個參數(shù)就是界面滑動過程的百分比口四,那么我們獲取上一個界面的導航欄透明度孵运、下一個界面的導航欄透明度、以及滑動的進度蔓彩,通過很簡單的數(shù)學計算就可以得出當前進度應該對應的透明度是多少了治笨,這里也可以看出我們給 ViewController 添加一個導航欄透明度屬性是多么有意義,這里就可以直接調(diào)用了粪小,當然大磺,要記得導入我們的Category:
// 交換的方法,監(jiān)控滑動手勢
- (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 中添加一個處理绳瘟,監(jiān)控松手后時自動完成返回還是取消返回操作,同時使用 UIView 動畫(關于 UIView 動畫可以看我的這篇文章:傳送門:iOS基礎動畫教程)姿骏,在自動操作的那個時間內(nèi)將透明度變?yōu)閷缑娴膶Ш綑谕该鞫忍巧屍渥兓牟荒敲刺S:
#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]];
}
不過意義不是特別大分瘦。
結(jié)
以上這些處理基本都在 Category 里寫代碼蘸泻,一次搞定,真正在自己的 ViewController 需要做的只是一句:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.navBarBgAlpha = @"0.0";
}
很簡單吧~更多效果有興趣的可以自己繼續(xù)修修補補嘲玫,這個過程也是很有意思的悦施。
再次宣傳,代碼可以在示例工程下載(覺得有幫助的小伙伴請不吝加Star~):https://github.com/Cloudox/SmoothNavDemo
參考(swift):http://www.reibang.com/p/454b06590cf1
關注我的公眾號【月亮與二進制】去团,鵝廠程序員的敲碼間隙抡诞,也能讀書觀影練劍寫字穷蛹,分享給你我的世界