源码分享:webview的goback过渡

前言

在iOS7之后,我们可能已经习惯了使用屏幕左边沿向右滑来返回上一级的页面。从开发的角度来说就是将当前的 viewControllernavigation 的栈中 pop出来。那如果我们将这个使用习惯保留到一个带 webviewviewController 中,也许我们的右滑只是为了退回到网页的上一级,并非是要退出当前的页面。这篇博客主要讲的就是这的实现。

需要的完成的效果

像iOS自带的Safari,或者微信内置的浏览器,都是实现了这样的功能的:

实现的主要原理

边缘滑动

我们右滑退出主要是有两种:

  • 只用从屏幕边沿滑动触发
  • 从任意位置右滑触发

显然,第一种是比较符合 iOS 的设计理念的,防止用户误滑。在iOS7之前,navigation 的右滑退出需要自己去实现,所以到现在还是有很多app的设计是沿用以前的任意位置滑动退出。比如新浪微博,在你看长微博时,很容易因为上滑而误触发右滑,这种体验是在一般。所以我们采用第一种。
来进行webview网页的回退,以及如果页面已经是根页面时,直接 popviewController

网页的回退

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]];
// 获取系统默认手势Handler并创建与之相同的手势
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 {
//禁用iOS系统自带的navigation右滑返回
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}

- (void) viewDidDisappear:(BOOL)animated {
//启用iOS系统自带的navigation右滑返回
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}

里添加。

最后的效果

demo地址

详情还是要源码吧。

iOSWebViewGoBackWithTransitionAnimation

最后的话

今天是七夕~我看朋友圈,一整天都是以下这副表情:

祝天下有情人,终成兄妹~(除了我,哈哈哈)~