前言
在iOS7之后,我们可能已经习惯了使用屏幕左边沿向右滑来返回上一级的页面。从开发的角度来说就是将当前的 viewController 从 navigation 的栈中 pop出来。那如果我们将这个使用习惯保留到一个带 webview 的 viewController 中,也许我们的右滑只是为了退回到网页的上一级,并非是要退出当前的页面。这篇博客主要讲的就是这的实现。
需要的完成的效果
像iOS自带的Safari,或者微信内置的浏览器,都是实现了这样的功能的:
实现的主要原理
边缘滑动
我们右滑退出主要是有两种:
显然,第一种是比较符合 iOS 的设计理念的,防止用户误滑。在iOS7之前,navigation 的右滑退出需要自己去实现,所以到现在还是有很多app的设计是沿用以前的任意位置滑动退出。比如新浪微博,在你看长微博时,很容易因为上滑而误触发右滑,这种体验是在一般。所以我们采用第一种。
来进行webview网页的回退,以及如果页面已经是根页面时,直接 pop 该 viewController。
网页的回退
iOS7之后,我们可以使用 UIScreenEdgePanGestureRecognizer 手势来识别从屏幕边沿滑动的手势:
1 2 3
| UIScreenEdgePanGestureRecognizer *popGesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesture:)]; [popGesture setEdges:UIRectEdgeLeft]; [self addGestureRecognizer:popGesture];
|
pop viewController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @implementation LZWebViewController { id navPanTarget; SEL navPanAction; IMP imp; }
- (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:[UIColor whiteColor]]; NSMutableArray *gestureTargets = [self.navigationController.interactivePopGestureRecognizer valueForKey:@"targets"]; id gestureTarget = [gestureTargets firstObject]; navPanTarget = [gestureTarget valueForKey:@"target"]; navPanAction = NSSelectorFromString(@"handleNavigationTransition:"); if (navPanTarget && [navPanTarget respondsToSelector:navPanAction]) { imp =[navPanTarget methodForSelector:navPanAction]; } CGFloat width = [UIScreen mainScreen].bounds.size.width; CGFloat height = [UIScreen mainScreen].bounds.size.height; LZWebView *webView = [[LZWebView alloc] initWithFrame:CGRectMake(0, 0, width,height)]; webView.lzPanWebViewDelegate = self; [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.url]]]; [self.view addSubview:webView]; }
- (void)LZWebView:(LZWebView *)webView panPopGesture:(UIPanGestureRecognizer *)pan { if (imp) { void (*func)(id, SEL, UIPanGestureRecognizer*) = (void *)imp; func(navPanTarget, navPanAction,pan); } }
|
这里有一个小插曲,
之前我是使用这样的方法调用:
1
| [navPanTarget performSelector:navPanAction withObject:pan];
|
编译器会警告我们:
PerformSelector may cause a leak because its selector is unknown
Stackoverflow解决相关链接
大致了解了一下:
在ARC模式下,运行时需要知道如何处理你正在调用的方法的返回值。这个返回值可以是任意值,如void,int,char,NSString,id等等。ARC通过头文件的函数定义来得到这些信息。所以平时我们用到的静态选择器就不会出现这个警告。因为在编译期间,这些信息都已经确定。
而使用[someController performSelector: NSSelectorFromString(@”someMethod”)];时ARC并不知道该方法的返回值是什么,以及该如何处理?该忽略?还是标记为ns_returns_retained还是ns_returns_autoreleased?
所以会有编译警告。
goback 过渡
我们使用 webview 的 goback
API时,它是一个退回上一个网页的操作,会有闪烁的感觉。并且我们不可获取上一个页面的显示状况,那是怎么做出丝滑滑动的效果的呢?
我们获取不到上一个页面的状态显示,但是我们可以在跳转到下一个页面之前截一张该页面的图。由于返回时的“伪装”。我细心把玩了微信,发现他也是这种实现原理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| * 屏幕截图 */ + (UIImage *)screenshotOfView:(UIView *)view { UIGraphicsBeginImageContextWithOptions(view.frame.size, YES, 0.0); [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
* 添加阴影效果 */ + (void)addShadowToView:(UIView *)view { CALayer *layer = view.layer; UIBezierPath *path = [UIBezierPath bezierPathWithRect:layer.bounds]; layer.shadowPath = path.CGPath; layer.shadowColor = [UIColor blackColor].CGColor; layer.shadowOffset = CGSizeZero; layer.shadowOpacity = 0.4f; layer.shadowRadius = 8.0f; }
|
用一个数组来管理当前 webview 的页面层级关系。具体可以查看demo。
动画效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| #pragma mark UIGestureDelegate - (void)panGesture:(UIPanGestureRecognizer *)sender { if (![self canGoBack] || historyStack.count == 0) { if (self.lzPanWebViewDelegate && [self.lzPanWebViewDelegate respondsToSelector:@selector(LZWebView:panPopGesture:)]) { [self.lzPanWebViewDelegate LZWebView:self panPopGesture:sender]; } return; } CGPoint translationPoint = [sender translationInView:self]; if (sender.state == UIGestureRecognizerStateBegan) { panStartX = translationPoint.x; }else if (sender.state == UIGestureRecognizerStateChanged) { CGFloat deltaX = translationPoint.x - panStartX; if (deltaX > 0) { if ([self canGoBack]) { assert([historyStack count] > 0); CGRect rc = self.frame; rc.origin.x = deltaX; self.frame = rc; [self historyView].image = [[historyStack lastObject] objectForKey:@"preview"]; rc.origin.x = -self.bounds.size.width/2.0f + deltaX/2.0f; [self historyView].frame = rc; } } } else if (sender.state == UIGestureRecognizerStateEnded) { CGFloat deltaX = translationPoint.x - panStartX; CGFloat duration = .5f; if ([self canGoBack]) { if (deltaX > self.bounds.size.width/4.0f) { [UIView animateWithDuration:(1.0f - deltaX/self.bounds.size.width)*duration animations:^{ CGRect rc = self.frame; rc.origin.x = self.bounds.size.width; self.frame = rc; rc.origin.x = 0; [self historyView].frame = rc; } completion:^(BOOL finished) { [self goBack]; CGRect rc = self.frame; rc.origin.x = 0; self.frame = rc; self.alpha = 0; }]; } else { [UIView animateWithDuration:(deltaX/self.bounds.size.width)*duration animations:^{ CGRect rc = self.frame; rc.origin.x = 0; self.frame = rc; rc.origin.x = -self.bounds.size.width/2.0f; [self historyView].frame = rc; } completion:^(BOOL finished) { }]; } } } }
|
相关协议
LZPanWebViewDelegate
当 webview 的页面不可再 goback 时,该协议中的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| - (void)LZWebView:(LZWebView *)webView panPopGesture:(UIPanGestureRecognizer *)pan; ```
会被回调。
#### webview 的 delegate
## 需要注意
我们所定义的手势会与系统的 interactivePopGestureRecognizer 形成冲突。可以先行禁止系统的。如下:
```objc self.navigationController.interactivePopGestureRecognizer.enabled = NO;
|
建议是在 :
1 2 3 4 5 6 7 8 9
| - (void)viewWillAppear:(BOOL)animated { self.navigationController.interactivePopGestureRecognizer.enabled = NO; }
- (void) viewDidDisappear:(BOOL)animated { self.navigationController.interactivePopGestureRecognizer.enabled = NO; }
|
里添加。
最后的效果
demo地址
详情还是要源码吧。
iOSWebViewGoBackWithTransitionAnimation
最后的话
今天是七夕~我看朋友圈,一整天都是以下这副表情:
祝天下有情人,终成兄妹~(除了我,哈哈哈)~