更新:最后提供的所謂“終極”解決方案,之前都是自己的項目在用,分享出來之后,發(fā)現(xiàn)有一些地方還需要改進(jìn)。但是總體思路不變,因此如非必要文章不會做大幅更改,最終代碼請以文末github地址為準(zhǔn),另,歡迎提供Bug
前言
前一段時間換了工作,公司項目趕得比較緊,沒有時間更新文章,現(xiàn)在閑下來了,趕緊寫一篇來彌補自己的羞愧。
今天我們來重點討論導(dǎo)航欄返回的問題,包括各種問題的解決方案。
系統(tǒng)默認(rèn)導(dǎo)航欄的返回按鈕和返回方式
在默認(rèn)情況下,導(dǎo)航欄返回按鈕長這個樣子
導(dǎo)航欄左上角的返回按鈕,其文本默認(rèn)為上一個ViewController的標(biāo)題,如果上一個ViewController沒有標(biāo)題,則為Back(中文環(huán)境下為“返回”)。
在默認(rèn)情況下,導(dǎo)航欄返回的點擊交互和滑動交互如下
這些東西不需要任何設(shè)置和操作,因此也沒有其他需要說明的地方。
自定義左上角的返回按鈕
絕大多數(shù)情況下,我們都需要根據(jù)產(chǎn)品需求自定義左上角的返回按鈕,雖然這對大多數(shù)開發(fā)者來說不是什么難事,但依然有幾個問題值得注意。
替換左上角返回按鈕
替換返回按鈕非常簡單,只需要在ViewController中創(chuàng)建一個UIBarButtonItem和一張圖片,并為按鈕添加相應(yīng)的點擊事件即可,代碼如下
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIButton * leftBtn = [UIButton buttonWithType:UIButtonTypeSystem];
leftBtn.frame = CGRectMake(0, 0, 25,25);
[leftBtn setBackgroundImage:[UIImage imageNamed:@"nav_back"] forState:UIControlStateNormal];
[leftBtn addTarget:self action:@selector(leftBarBtnClicked:) forControlEvents:UIControlEventTouchUpInside];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]initWithCustomView:leftBtn];
}
- (void)leftBarBtnClicked:(UIButton *)btn
{
[self.navigationController popViewControllerAnimated:YES];
}
我們來看一眼效果
調(diào)整按鈕位置
我們可以看到,上面的按鈕是有點偏右的,那如果我們想調(diào)整按鈕的位置該怎么做呢?設(shè)置Frame顯然是行不通的,因為導(dǎo)航欄的NavigationItem是個比較特殊的View,我們無法通過簡單的調(diào)整Frame來的調(diào)整左右按鈕的位置。但是在蘋果提供的UIButtonBarItem
中有個叫做UIBarButtonSystemItemFixedSpace
的控件,利用它,我們就可以輕松調(diào)整返回按鈕的位置。具體使用方法如下
//創(chuàng)建返回按鈕
UIButton * leftBtn = [UIButton buttonWithType:UIButtonTypeSystem];
leftBtn.frame = CGRectMake(0, 0, 25,25);
[leftBtn setBackgroundImage:[UIImage imageNamed:@"icon_back"] forState:UIControlStateNormal];
[leftBtn addTarget:self action:@selector(leftBarBtnClicked:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem * leftBarBtn = [[UIBarButtonItem alloc]initWithCustomView:leftBtn];;
//創(chuàng)建UIBarButtonSystemItemFixedSpace
UIBarButtonItem * spaceItem = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
//將寬度設(shè)為負(fù)值
spaceItem.width = -15;
//將兩個BarButtonItem都返回給NavigationItem
self.navigationItem.leftBarButtonItems = @[spaceItem,leftBarBtn];
我們來看一眼效果
可以看到,我們的返回按鈕已經(jīng)緊靠著屏幕邊緣。
這個方法同樣適用于調(diào)整導(dǎo)航欄右側(cè)的按鈕
讓滑動返回手勢生效
如果使用自定義的按鈕去替換系統(tǒng)默認(rèn)返回按鈕,會出現(xiàn)滑動返回手勢失效的情況。解決方法也很簡單,只需要重新添加導(dǎo)航欄的interactivePopGestureRecognizer
的delegate
即可。
首先為ViewContoller添加UIGestureRecognizerDelegate
協(xié)議
然后設(shè)置代理
self.navigationController.interactivePopGestureRecognizer.delegate = self;
至此,我們已經(jīng)將返回按鈕替換為我們的自定義按鈕,并使滑動返回重新生效。接下來,我們繼續(xù)來解決交互上的問題。
全屏滑動返回
這個一個很常見的需求,網(wǎng)上解決方案也很多,這里將本人常用的方法貼到這里。僅供參考
實現(xiàn)全屏滑動返回僅需在導(dǎo)航欄給導(dǎo)航欄添加UIGestureRecognizerDelegate
協(xié)議,并在ViewDidLoad中寫入如下代碼
// 獲取系統(tǒng)自帶滑動手勢的target對象
id target = self.interactivePopGestureRecognizer.delegate;
// 創(chuàng)建全屏滑動手勢,調(diào)用系統(tǒng)自帶滑動手勢的target的action方法
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:target action:@selector(handleNavigationTransition:)];
// 設(shè)置手勢代理,攔截手勢觸發(fā)
pan.delegate = self;
// 給導(dǎo)航控制器的view添加全屏滑動手勢
[self.view addGestureRecognizer:pan];
// 禁止使用系統(tǒng)自帶的滑動手勢
self.interactivePopGestureRecognizer.enabled = NO;
我們來看一眼效果(注意鼠標(biāo)位置)
成功
這種方法的原理其實很簡單,其實就是自定義一個全屏滑動手勢,并將滑動事件設(shè)置為系統(tǒng)滑動事件,然后禁用系統(tǒng)滑動手勢即可。
handleNavigationTransition
就是系統(tǒng)滑動的方法,雖然系統(tǒng)并未提供接口,但是我們我們可以通過runtime找到這個方法,因此直接調(diào)用即可。兩位,不必?fù)?dān)心什么私有API之類的問題,蘋果如果按照方法名去判斷是否使用私有API,那得誤傷多少App。
NavigationBar切換動畫的“終極解決方案”
本部分文字代碼都較多,不想看這么多廢話的同學(xué)請直接翻到末尾,文末附有下載地址,導(dǎo)入項目后,繼承即可生效。
在改變了導(dǎo)航欄樣式,實現(xiàn)了全屏滑動返回之后,我們有了一個看起來還不錯的導(dǎo)航欄。但是我們滑動時的切換依然是系統(tǒng)自帶的動畫,如果遇到前一個界面的NavigationBar為透明或前后兩個Bar顏色不一樣,這種漸變式的動畫看起來就會不太友好,尤其當(dāng)前后兩個界面其中一個界面的NavigationBar
為透明或隱藏時,其效果更是慘不忍睹。
這個問題,其實很多App,比如天貓、美團(tuán)等都通過一種“整體返回”的效果來解決這個問題。效果如下:
這種解決方案等于將兩個NavigationBar獨立開來,因此可以相對完美的解決導(dǎo)航欄滑動切換中的種種Bug。
接下來,我們來看看如何實現(xiàn)這種效果。
基本原理
以我個人的認(rèn)知,實現(xiàn)這個效果有三種基本思路:
- 使用
UINavigationController
自帶的setNavigationBarHidden: animated:
方法來實現(xiàn),每次push或pop時,在當(dāng)前控制器的viewWillDisappear:
中設(shè)置隱藏,在要跳轉(zhuǎn)的控制器的viewWillAppear:
中設(shè)置導(dǎo)航欄顯示。
- 在每次Push前對當(dāng)前頁面進(jìn)行截圖并保存到數(shù)組,Pop時取數(shù)組最后一個元素顯示,滑動結(jié)束后調(diào)用系統(tǒng)Pop方法并刪除最后一張截圖。
- 使用iOS 7之后開放的,UIViewControllerAnimatedTransitioning協(xié)議,來實現(xiàn)自定義導(dǎo)航欄轉(zhuǎn)場動畫及交互。
以上三種方法,方法一十分繁瑣,而且會有很多莫名其妙的BUG,直接pass。
在iOS的交互中,push一般通過按鈕的點擊事件或View的tap
事件觸發(fā),而pop則可能通過事件觸發(fā),也可能通過右滑手勢觸發(fā)。因此,我們將這個我們要實現(xiàn)的動畫效果分為交互效果和無交互效果兩種,下面我們將使用方法2和方法3提供的思路,分別實現(xiàn)這兩種效果,這樣就能較為完美的解決Push和Pop的動畫問題。
實現(xiàn)交互動畫效果
準(zhǔn)備需要使用的數(shù)組及手勢
#define ScreenWidth [UIScreen mainScreen].bounds.size.width
#define ScreenHeight [UIScreen mainScreen].bounds.size.height
@interface LTNavigationController ()<UIGestureRecognizerDelegate>
@property(strong,nonatomic)UIImageView * screenshotImgView;
@property(strong,nonatomic)UIView * coverView;
@property(strong,nonatomic)NSMutableArray * screenshotImgs;
@property(strong,nonatomic)UIPanGestureRecognizer *panGestureRec;
@end
@implementation LTNavigationController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 1,創(chuàng)建Pan手勢識別器,并綁定監(jiān)聽方法
_panGestureRec = [[UIScreenEdgePanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureRec:)];
_panGestureRec.edges = UIRectEdgeLeft;
// 為導(dǎo)航控制器的view添加Pan手勢識別器
[self.view addGestureRecognizer:_panGestureRec];
// 2.創(chuàng)建截圖的ImageView
_screenshotImgView = [[UIImageView alloc] init];
// app的frame是包括了狀態(tài)欄高度的frame
_screenshotImgView.frame = CGRectMake(0, 0, ScreenWidth, ScreenHeight);
// 3.創(chuàng)建截圖上面的黑色半透明遮罩
_coverView = [[UIView alloc] init];
// 遮罩的frame就是截圖的frame
_coverView.frame = _screenshotImgView.frame;
// 遮罩為黑色
_coverView.backgroundColor = [UIColor blackColor];
// 4.存放所有的截圖數(shù)組初始化
_screenshotImgs = [NSMutableArray array];
}
實現(xiàn)手勢的相應(yīng)事件
// 響應(yīng)手勢的方法
- (void)panGestureRec:(UIPanGestureRecognizer *)panGestureRec
{
// 如果當(dāng)前顯示的控制器已經(jīng)是根控制器了,不需要做任何切換動畫,直接返回
if(self.visibleViewController == self.viewControllers[0]) return;
// 判斷pan手勢的各個階段
switch (panGestureRec.state) {
case UIGestureRecognizerStateBegan:
// 開始拖拽階段
[self dragBegin];
break;
case UIGestureRecognizerStateEnded:
// 結(jié)束拖拽階段
[self dragEnd];
break;
default:
// 正在拖拽階段
[self dragging:panGestureRec];
break;
}
}
#pragma mark 開始拖動,添加圖片和遮罩
- (void)dragBegin
{
// 重點,每次開始Pan手勢時,都要添加截圖imageview 和 遮蓋cover到window中
[self.view.window insertSubview:_screenshotImgView atIndex:0];
[self.view.window insertSubview:_coverView aboveSubview:_screenshotImgView];
// 并且,讓imgView顯示截圖數(shù)組中的最后(最新)一張截圖
_screenshotImgView.image = [_screenshotImgs lastObject];
//_screenshotImgView.transform = CGAffineTransformMakeTranslation(ScreenWidth, 0);
}
// 默認(rèn)的將要變透明的遮罩的初始透明度(全黑)
#define kDefaultAlpha 0.6
// 當(dāng)拖動的距離,占了屏幕的總寬高的3/4時, 就讓imageview完全顯示,遮蓋完全消失
#define kTargetTranslateScale 0.75
#pragma mark 正在拖動,動畫效果的精髓,進(jìn)行位移和透明度變化
- (void)dragging:(UIPanGestureRecognizer *)pan
{
// 得到手指拖動的位移
CGFloat offsetX = [pan translationInView:self.view].x;
// 讓整個view都平移 // 挪動整個導(dǎo)航view
if (offsetX > 0) {
self.view.transform = CGAffineTransformMakeTranslation(offsetX, 0);
}
// 計算目前手指拖動位移占屏幕總的寬高的比例,當(dāng)這個比例達(dá)到3/4時, 就讓imageview完全顯示,遮蓋完全消失
double currentTranslateScaleX = offsetX/self.view.frame.size.width;
if (offsetX < ScreenWidth) {
_screenshotImgView.transform = CGAffineTransformMakeTranslation((offsetX - ScreenWidth) * 0.6, 0);
}
// 讓遮蓋透明度改變,直到減為0,讓遮罩完全透明,默認(rèn)的比例-(當(dāng)前平衡比例/目標(biāo)平衡比例)*默認(rèn)的比例
double alpha = kDefaultAlpha - (currentTranslateScaleX/kTargetTranslateScale) * kDefaultAlpha;
_coverView.alpha = alpha;
}
#pragma mark 結(jié)束拖動,判斷結(jié)束時拖動的距離作相應(yīng)的處理,并將圖片和遮罩從父控件上移除
- (void)dragEnd
{
// 取出挪動的距離
CGFloat translateX = self.view.transform.tx;
// 取出寬度
CGFloat width = self.view.frame.size.width;
if (translateX <= 40) {
// 如果手指移動的距離還不到屏幕的一半,往左邊挪 (彈回)
[UIView animateWithDuration:0.3 animations:^{
// 重要~~讓被右移的view彈回歸位,只要清空transform即可辦到
self.view.transform = CGAffineTransformIdentity;
// 讓imageView大小恢復(fù)默認(rèn)的translation
_screenshotImgView.transform = CGAffineTransformMakeTranslation(-ScreenWidth, 0);
// 讓遮蓋的透明度恢復(fù)默認(rèn)的alpha 1.0
_coverView.alpha = kDefaultAlpha;
} completion:^(BOOL finished) {
// 重要,動畫完成之后,每次都要記得 移除兩個view,下次開始拖動時,再添加進(jìn)來
[_screenshotImgView removeFromSuperview];
[_coverView removeFromSuperview];
}];
} else {
// 如果手指移動的距離還超過了屏幕的一半,往右邊挪
[UIView animateWithDuration:0.3 animations:^{
// 讓被右移的view完全挪到屏幕的最右邊,結(jié)束之后,還要記得清空view的transform
self.view.transform = CGAffineTransformMakeTranslation(width, 0);
// 讓imageView位移還原
_screenshotImgView.transform = CGAffineTransformMakeTranslation(0, 0);
// 讓遮蓋alpha變?yōu)?,變得完全透明
_coverView.alpha = 0;
} completion:^(BOOL finished) {
// 重要~~讓被右移的view完全挪到屏幕的最右邊,結(jié)束之后,還要記得清空view的transform,不然下次再次開始drag時會出問題,因為view的transform沒有歸零
self.view.transform = CGAffineTransformIdentity;
// 移除兩個view,下次開始拖動時,再加回來
[_screenshotImgView removeFromSuperview];
[_coverView removeFromSuperview];
// 執(zhí)行正常的Pop操作:移除棧頂控制器,讓真正的前一個控制器成為導(dǎo)航控制器的棧頂控制器
[self popViewControllerAnimated:NO];
}];
}
}
實現(xiàn)截圖保存功能,并在Push前截圖
- (void)screenShot
{
// 將要被截圖的view,即窗口的根控制器的view
UIViewController *beyondVC = self.view.window.rootViewController;
// 背景圖片 總的大小
CGSize size = beyondVC.view.frame.size;
// 開啟上下文,使用參數(shù)之后,截出來的是原圖(YES 0.0 質(zhì)量高)
UIGraphicsBeginImageContextWithOptions(size, YES, 0.0);
// 要裁剪的矩形范圍
CGRect rect = CGRectMake(0, 0, ScreenWidth, ScreenHeight);
//注:iOS7以后renderInContext:由drawViewHierarchyInRect:afterScreenUpdates:替代
[beyondVC.view drawViewHierarchyInRect:rect afterScreenUpdates:NO];
// 從上下文中,取出UIImage
UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();
// 添加截取好的圖片到圖片數(shù)組
if (snapshot) {
[_screenshotImgs addObject:snapshot];
}
// 千萬記得,結(jié)束上下文(移除棧頂?shù)幕诋?dāng)前位圖的圖形上下文)
UIGraphicsEndImageContext();
}
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
//有在導(dǎo)航控制器里面有子控制器的時候才需要截圖
if (self.viewControllers.count >= 1) {
// 調(diào)用自定義方法,使用上下文截圖
[self screenShot];
}
// 截圖完畢之后,才調(diào)用父類的push方法
[super pushViewController:viewController animated:YES];
}
重寫常用的pop方法
在一開始基本原理地方,我們說過pop時要刪除最后一張截圖,用來保證數(shù)組中的最后一張截圖是上一個控制器,但是很多情況下我們可能調(diào)用的是導(dǎo)航欄的popToViewController: animated:
方法或popToRootViewControllerAnimated:
來返回,這種情況下,我們刪除的可能就不是一張截圖,因此我們需要分別重寫這些Pop方法,去確定我們要刪除多少張圖片,代碼如下
- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
[_screenshotImgs removeLastObject];
return [super popViewControllerAnimated:animated];
}
- (NSArray<UIViewController *> *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
{
for (NSInteger i = self.viewControllers.count - 1; i > 0; i--) {
if (viewController == self.viewControllers[i]) {
break;
}
[_screenshotImgs removeLastObject];
}
return [super popToViewController:viewController animated:animated];
}
- (NSArray<UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated
{
[_screenshotImgs removeAllObjects];
return [super popToRootViewControllerAnimated:animated];
}
※在指定的控制器屏蔽手勢
在上面代碼中,我們使用的是側(cè)滑手勢,并將相應(yīng)區(qū)域設(shè)置為屏幕左側(cè)。
之所以不用全屏滑動,是因為全屏滑動手勢在有些時候會和其他手勢沖突,如果沖突的是我們自定義的手勢,自然好解決,但如果是系統(tǒng)手勢,如TableView的左滑菜單操作,這個事情就很蛋疼的。
但是如果必須要做全屏滑動手勢的話,我們可以對代碼稍作修改,某些控制器中屏蔽手勢。
首先給導(dǎo)航欄添加禁用名單數(shù)組并配置
...
@property(nonatomic,copy)NSArray * forbiddenArray;
...
- (void)viewDidLoad {
[super viewDidLoad];
//原來代碼
...
//將手勢禁用,之后在Push時根據(jù)條件開啟
self.panGestureRec.enabled = enable
//將需要禁用手勢的控制器的類名加到這個數(shù)組
self.forbiddenArray = @[@"SCViewController",@"ManageAddressViewController"];
}
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
//在指定控制器中禁用手勢 解決滑動返回手勢和某些手勢沖突問題
BOOL enable = YES;
for (NSString * string in self.forbiddenArray) {
NSString * className = NSStringFromClass([viewController class]);
if ([string isEqualToString:className]) {
enable = NO;
}
}
self.panGestureRec.enabled = enable;
//原有代碼
...
}
- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
NSInteger count = self.viewControllers.count;
NSString * className = nil;
if (count >= 2) {
className = NSStringFromClass([self.viewControllers[count -2] class]);
}
BOOL enable = YES;
for (NSString * string in self.forbiddenArray) {
if ([string isEqualToString:className]) {
enable = NO;
}
}
self.panGestureRec.enabled = enable;
//原有代碼
...
return [super popViewControllerAnimated:animated];
}
到了這里,我們已經(jīng)完成了交互式的切換動畫,效果跟開頭一樣,就不再截圖。接下來我們來解決另一個大Boss-非交互式動畫
實現(xiàn)非交互動畫效果
理論基礎(chǔ)
這里我們就要用到之前說的UIViewControllerAnimatedTransitioning
來實現(xiàn)。限于篇幅,這里不再詳細(xì)介紹這部分的基礎(chǔ)知識,大家可以移步這兩篇博客做一個初步的了解
向 UINavigationController 的傳統(tǒng)動畫說”再見” — 自定義過場動畫(一)
iOS 7:自定義導(dǎo)航轉(zhuǎn)場動畫以及更多
實現(xiàn)原理
注:FromVC代表即將消失的視圖控制器,ToVC表示將要展示的視圖控制器
我們要實現(xiàn)的效果:
Push的時候,F(xiàn)romVC往左移動,ToVC從屏幕右側(cè)出現(xiàn)跟隨FromVC左移直至FromVC消失,此時ToVC剛好完整顯示在屏幕上。
Pop的時候,F(xiàn)romVC向右移動,ToVC從屏幕邊緣出現(xiàn)跟隨FromVC向右移動直至FromVC消失,此時ToVC剛好完整顯示在屏幕上
實現(xiàn)的時候,我們依然需要將Push和Pop分開討論
先說Pop
1.和交互式動畫一樣,每次Push時對屏幕截屏并保存,Pop的再次截屏但不保存
2.把Pop時截取的圖片作為FromVC展示,把Push到這個界面時截取的圖片作為ToVC展示
3.并對兩張圖片做位移動畫,動畫結(jié)束后移除兩張圖片
然后是Push
1.Push時先對當(dāng)前屏幕截屏。
2.將截取的圖片保存方便Pop回來時使用,并把這張圖片作為這次Push的FromVC保存。
3.獲取當(dāng)前導(dǎo)航欄控制器對象,調(diào)整其Transform屬性中的位移參數(shù)作為ToVC展示
4.對截圖和導(dǎo)航欄做位移,動畫結(jié)束后直接移除截屏圖片
為什么要對導(dǎo)航欄作位移?
首先,在Push結(jié)束之前,我們是無法知道ToVC具體是什么樣子,系統(tǒng)的截屏方法對于未加載出來的View是無能為力的,而UIView的
snapshotViewAfterScreenUpdates:
方法又無法帶著導(dǎo)航欄一起映射到一個新的View上,因此視覺效果很差。
正好在Pop的時候,為了達(dá)到想要的動畫效果,用來展示的兩張圖片都需要放到導(dǎo)航欄的View上,因此在Push的時候我們就直接將導(dǎo)航欄的View做一個放射變換,當(dāng)然,這也就意味著,當(dāng)我們Push的時候,截屏就不能再放到導(dǎo)航欄上,而是應(yīng)該放到它的“更上一層“ --UITabbarController
的View上
讓我們擼一發(fā)代碼
根據(jù)上述實現(xiàn)原理,我們可以知道,我們的主要工作重點在于打造一個合適的動畫控制器。更準(zhǔn)確的說,我們需要實現(xiàn)的細(xì)節(jié)都在UIViewControllerAnimatedTransitioning
中,由于之前解釋的很詳細(xì),這里我直接貼上相應(yīng)代碼供參考
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIImageView * screentImgView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenHeight)];
UIImage * screenImg = [self screenShot];
screentImgView.image =screenImg;
//取出fromViewController,fromView和toViewController,toView
UIViewController * fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
// UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
UIViewController * toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView * toView = [transitionContext viewForKey:UITransitionContextToViewKey];
CGRect fromViewEndFrame = [transitionContext finalFrameForViewController:fromViewController];
fromViewEndFrame.origin.x = ScreenWidth;
CGRect fromViewStartFrame = fromViewEndFrame;
CGRect toViewEndFrame = [transitionContext finalFrameForViewController:toViewController];
CGRect toViewStartFrame = toViewEndFrame;
UIView * containerView = [transitionContext containerView];
if (self.navigationOperation == UINavigationControllerOperationPush) {
[self.screenShotArray addObject:screenImg];
//toViewStartFrame.origin.x += ScreenWidth;
[containerView addSubview:toView];
toView.frame = toViewStartFrame;
UIView * nextVC = [[UIView alloc]initWithFrame:CGRectMake(ScreenWidth, 0, ScreenWidth, ScreenHeight)];
//[nextVC addSubview:[toView snapshotViewAfterScreenUpdates:YES]];
[self.navigationController.tabBarController.view insertSubview:screentImgView atIndex:0];
//[self.navigationController.tabBarController.view addSubview:nextVC];
nextVC.layer.shadowColor = [UIColor blackColor].CGColor;
nextVC.layer.shadowOffset = CGSizeMake(-0.8, 0);
nextVC.layer.shadowOpacity = 0.6;
self.navigationController.view.transform = CGAffineTransformMakeTranslation(ScreenWidth, 0);
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
//toView.frame = toViewEndFrame;
self.navigationController.view.transform = CGAffineTransformMakeTranslation(0, 0);
screentImgView.center = CGPointMake(-ScreenWidth/2, ScreenHeight / 2);
//nextVC.center = CGPointMake(ScreenWidth/2, ScreenHeight / 2);
} completion:^(BOOL finished) {
[nextVC removeFromSuperview];
[screentImgView removeFromSuperview];
[transitionContext completeTransition:YES];
}];
}
if (self.navigationOperation == UINavigationControllerOperationPop) {
fromViewStartFrame.origin.x = 0;
[containerView addSubview:toView];
//若removeCount大于0 則說明Pop了不止一個控制器
if (_removeCount > 0) {
for (NSInteger i = 0; i < _removeCount; i ++) {
if (i == _removeCount - 1) {
//當(dāng)刪除到要跳轉(zhuǎn)頁面的截圖時,不再刪除,并將該截圖作為ToVC的截圖展示
lastVcImgView.image = [self.screenShotArray lastObject];
_removeCount = 0;
break;
}
else
{
[self.screenShotArray removeLastObject];
}
}
}
else
{
lastVcImgView.image = [self.screenShotArray lastObject];
}
lastVcImgView.image = [self.screenShotArray lastObject];
screentImgView.layer.shadowColor = [UIColor blackColor].CGColor;
screentImgView.layer.shadowOffset = CGSizeMake(-0.8, 0);
screentImgView.layer.shadowOpacity = 0.6;
[self.navigationController.tabBarController.view addSubview:lastVcImgView];
[self.navigationController.tabBarController.view addSubview:screentImgView];
// fromView.frame = fromViewStartFrame;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
screentImgView.center = CGPointMake(ScreenWidth * 3 / 2 , ScreenHeight / 2);
lastVcImgView.center = CGPointMake(ScreenWidth/2, ScreenHeight/2);
//fromView.frame = fromViewEndFrame;
} completion:^(BOOL finished) {
//[self.navigationController setNavigationBarHidden:NO];
[lastVcImgView removeFromSuperview];
[screentImgView removeFromSuperview];
[self.screenShotArray removeLastObject];
[transitionContext completeTransition:YES];
}];
}
}
- (void)removeLastScreenShot
{
[self.screenShotArray removeLastObject];
}
- (UIImage *)screenShot
{
// 將要被截圖的view,即窗口的根控制器的view(必須不含狀態(tài)欄,默認(rèn)ios7中控制器是包含了狀態(tài)欄的)
UIViewController *beyondVC = self.navigationController.view.window.rootViewController;
// 背景圖片 總的大小
CGSize size = beyondVC.view.frame.size;
// 開啟上下文,使用參數(shù)之后,截出來的是原圖(YES 0.0 質(zhì)量高)
UIGraphicsBeginImageContextWithOptions(size, YES, 0.0);
// 要裁剪的矩形范圍
CGRect rect = CGRectMake(0, 0, ScreenWidth, ScreenHeight);
//注:iOS7以后renderInContext:由drawViewHierarchyInRect:afterScreenUpdates:替代
[beyondVC.view drawViewHierarchyInRect:rect afterScreenUpdates:NO];
// 從上下文中,取出UIImage
UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext();
// 千萬記得,結(jié)束上下文(移除棧頂?shù)幕诋?dāng)前位圖的圖形上下文)
UIGraphicsEndImageContext();
// 返回截取好的圖片
return snapshot;
}
注:
removeLastScreenShot
需要在使用滑動手勢Pop后調(diào)用,用來清除動畫控制器中保存的截圖,否則當(dāng)交互式和非交互式動畫交替使用時,會出現(xiàn)截圖混亂的問題。
更新:
在調(diào)用 popToViewController:(UIViewController *)viewController animated:(BOOL)animated
一次Pop多個頁面,或調(diào)用popToRootViewControllerAnimated
直接回到跟控制器時,一樣需要清除對應(yīng)數(shù)量的截圖,并且需要和導(dǎo)航欄配合操作。新的代碼已提交github,文章里也已經(jīng)更新動畫控制器對應(yīng)的部分,具體代碼還是以GitHub為準(zhǔn)。
看看效果
我們將動畫持續(xù)時間調(diào)制兩秒,觀察一下效果
后記
這篇文章開始于四個月之前,中間由于個人以及工作原因拖了又拖,終于在最近補完,邏輯混亂之處請見諒。
制作完成的導(dǎo)航欄和動畫控制器的下載地址
導(dǎo)航欄和動畫控制器下載地址
使用方法:
1.將這四個文件導(dǎo)入工程
2.將需要動畫的導(dǎo)航欄繼承KLTNavigationController即可
如果我的文章對您有幫助,請點贊或評論,謝謝!