Files
knowledge-kit/Chapter1 - iOS/1.32.md
2020-02-25 17:46:51 +08:00

304 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 终极截屏
- **-(void)snapshotForView:(__kindof UIView *)view;**
今天新学到这种写法__kindof 是苹果声明的一个特性,是 Xcode7 出现的新特性。
- 假如我们想声明一个方法,这个方法的参数必须是一个 UIView 类型的对象,那么我们应该可以写成下面这个样子
```
-(void)snapshotForView:(UIView *)view;
```
- 那么我们想声明一个方法,这个方法的参数必须是 UIView 及其 UIView 的子类,那么前一种写法就满足不了我们的需求了,这时候引入了 __kindof 方法
```
-(void)snapshotForView:(__kindof UIView *)view;
```
**UIWebView 截图**
对 UIWebView 截图比较简单renderInContext 这个方法相信大家都不会陌生,这个方法是 CALayer 的一个实例方法,可以用来对大部分 View 进行截图。我们知道UIWebView 承载内容的其实是作为其子 View 的 UIScrollView所以对 UIWebView 截图应该对其 scrollView 进行截图。具体的截图方法如下:
```
- (void)snapshotForScrollView:(UIScrollView *)scrollView
{
// 1. 记录当前 scrollView 的偏移和位置
CGPoint currentOffset = scrollView.contentOffset;
CGRect currentFrame = scrollView.frame;
scrollView.contentOffset = CGPointZero;
// 2. 将 scrollView 展开为其实际内容的大小
scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
// 3. 第三个参数设置为 0 表示设置为屏幕的默认缩放因子
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, 0);
[scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 4. 重新设置 scrollView 的偏移和位置,还原现场
scrollView.contentOffset = currentOffset;
scrollView.frame = currentFrame;
}
```
**WKWebView 截图**
虽然 WKWebView 里也有 scrollView但是直接对这个 scrollView 截图得到的是一片空白的,具体原因不明。一番 Google 之后可以看到好些人提到 drawViewHierarchyInRect 方法, 可以看到这个方法是 iOS 7.0 开始引入的。官方文档中描述为:
> Renders a snapshot of the complete view hierarchy as visible onscreen into the current context.
注意其中的 **visible onscreen**,也就是将屏幕中可见部分渲染到上下文中,这也解释了为什么对 WKWebView 中的 scrollView 展开为实际内容大小,再调用 drawViewHierarchyInRect 方法总是得到一张不完整的截图(只有屏幕可见区域被正确截到,其他区域为空白)。
不过,这样倒是给我们提供了一个思路,可以将 WKWebView 按屏幕高度裁成 n 页,然后将 WKWebView 一页一页的往上推,每推一页就调用一次 drawViewHierarchyInRect 将当前屏幕的截图渲染到上下文中,最后调用 UIGraphicsGetImageFromCurrentImageContext 从上下文中获取的图片即为完整截图。
核心代码如下:
```
- (void)snapshotForWKWebView:(WKWebView *)webView
{
// 1
UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
[webView.superview addSubview:snapshotView];
// 2
CGPoint currentOffset = webView.scrollView.contentOffset;
...
// 3
UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds];
[webView removeFromSuperview];
[containerView addSubview:webView];
// 4
CGSize totalSize = webView.scrollView.contentSize;
NSInteger page = ceil(totalSize.height / containerView.bounds.size.height);
webView.scrollView.contentOffset = CGPointZero;
webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height);
UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale);
[self drawContentPage:0 maxIndex:page completion:^{
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 8
[webView removeFromSuperview];
...
}];
}
- (void)drawContentPage(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion
{
// 5
CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(containerView.bounds), containerView.bounds.size.width, containerView.frame.size.height);
CGRect myFrame = webView.frame;
myFrame.origin.y = -(index * containerView.frame.size.height);
webView.frame = myFrame;
// 6
[targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES];
// 7
if (index < maxIndex) {
[self drawContentPage:index + 1 maxIndex:maxIndex completion:completion];
} else {
completion();
}
}
```
代码注意项如下(对应代码注释中的序号):
1. 为了截图时对 frame 进行操作不会出现闪屏等现象,我们需要盖一个“假”的 webView 到现在的位置上,并将真正的 webView “摘下来”。调用 snapshotViewAfterScreenUpdates 即可得到这样一个“假”的 webView
2. 保存真正的 webView 的偏移、位置等信息,以便截图完成之后“还原现场”
3. 用一个新的视图承载“真正的” webView这个视图也是绘图所用到的上下文
4. 将 webView 按照实际内容高度和屏幕高度分成 page 页
5. 得到每一页的实际位置,并将 webView 往上推到该位置
6. 调用 drawViewHierarchyInRect 将当前位置的 webView 渲染到上下文中
7. 如果还未到达最后一页,则递归调用 drawViewHierarchyInRect 方法进行渲染;如果已经渲染完了全部页,则回调通知截图完成
8. 调用 UIGraphicsGetImageFromCurrentImageContext 方法从当前上下文中获取到完整截图,将第 2 步中保存的信息重新赋予到 webView 上,“还原现场”
注意:我们的截图方法中有对 webView 的 frame 进行操作,如果其他地方如果有对 frame 进行操作的话,是会影响我们截图的。所以在截图时应该禁用掉其他地方对 frame 的改变,就像这样:
```
- (void)layoutWebView
{
if (!_isCapturing) {
self.wkWebView.frame = [self frameForWebView];
}
}
```
```
#import <UIKit/UIKit.h>
@class PPSnapshotHandler;
@protocol PPSnapshotHandlerDelegate <NSObject>
@optional
- (void)snapshotHandler:(PPSnapshotHandler *)snapshotHandler didFinish:(UIImage *)captureImage forView:(UIView *)view;
@end
@interface PPSnapshotHandler : NSObject
+ (instancetype)defaultHandler;
@property (nonatomic, weak) id<PPSnapshotHandlerDelegate> delegate;
- (void)snapshotForView:(__kindof UIView *)view;
@end
#import "PPSnapshotHandler.h"
#import <WebKit/WebKit.h>
#define DELAY_TIME_DRAW 0.1
@interface PPSnapshotHandler () {
BOOL _isCapturing;
UIView *_captureView;
}
@end
@implementation PPSnapshotHandler
+ (instancetype)defaultHandler
{
static dispatch_once_t onceToken;
static PPSnapshotHandler *defaultHandler = nil;
dispatch_once(&onceToken, ^{
defaultHandler = [[PPSnapshotHandler alloc] init];
});
return defaultHandler;
}
#pragma mark - public method
- (void)snapshotForView:(__kindof UIView *)view
{
if (!view || _isCapturing) {
return;
}
_captureView = view;
if ([view isKindOfClass:[UIScrollView class]]) {
[self snapshotForScrollView:(UIScrollView *)view];
} else if ([view isKindOfClass:[UIWebView class]]) {
UIWebView *webView = (UIWebView *)view;
[self snapshotForScrollView:webView.scrollView];
} else if ([view isKindOfClass:[WKWebView class]]) {
[self snapshotForWKWebView:(WKWebView *)view];
}
}
#pragma mark - WKWebView
- (void)snapshotForWKWebView:(WKWebView *)webView
{
UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
snapshotView.frame = webView.frame;
[webView.superview addSubview:snapshotView];
CGPoint currentOffset = webView.scrollView.contentOffset;
CGRect currentFrame = webView.frame;
UIView *currentSuperView = webView.superview;
NSUInteger currentIndex = [webView.superview.subviews indexOfObject:webView];
UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds];
[webView removeFromSuperview];
[containerView addSubview:webView];
CGSize totalSize = webView.scrollView.contentSize;
NSInteger page = ceil(totalSize.height / containerView.bounds.size.height);
webView.scrollView.contentOffset = CGPointZero;
webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height);
UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale);
[self drawContentPage:containerView webView:webView index:0 maxIndex:page completion:^{
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[webView removeFromSuperview];
[currentSuperView insertSubview:webView atIndex:currentIndex];
webView.frame = currentFrame;
webView.scrollView.contentOffset = currentOffset;
[snapshotView removeFromSuperview];
self->_isCapturing = NO;
if (self.delegate && [self.delegate respondsToSelector:@selector(snapshotHandler:didFinish:forView:)]) {
[self.delegate snapshotHandler:self didFinish:snapshotImage forView:self->_captureView];
}
}];
}
- (void)drawContentPage:(UIView *)targetView webView:(WKWebView *)webView index:(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion
{
CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(targetView.bounds), targetView.bounds.size.width, targetView.frame.size.height);
CGRect myFrame = webView.frame;
myFrame.origin.y = -(index * targetView.frame.size.height);
webView.frame = myFrame;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(DELAY_TIME_DRAW * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES];
if (index < maxIndex) {
[self drawContentPage:targetView webView:webView index:index + 1 maxIndex:maxIndex completion:completion];
} else {
completion();
}
});
}
#pragma mark - UIScrollView
- (void)snapshotForScrollView:(UIScrollView *)scrollView
{
CGPoint currentOffset = scrollView.contentOffset;
CGRect currentFrame = scrollView.frame;
scrollView.contentOffset = CGPointZero;
scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, UIScreen.mainScreen.scale);
[scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
scrollView.contentOffset = currentOffset;
scrollView.frame = currentFrame;
if (self.delegate && [self.delegate respondsToSelector:@selector(snapshotHandler:didFinish:forView:)]) {
[self.delegate snapshotHandler:self didFinish:snapshotImage forView:_captureView];
}
}
@end
```