ios 获取 UIScrollView 的屏幕截图,包括屏幕外部分
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3539717/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Getting a screenshot of a UIScrollView, including offscreen parts
提问by Tim Sullivan
I have a UIScrollView
decendent that implements a takeScreenshot method that looks like this:
我有一个UIScrollView
继承人,它实现了一个像这样的 takeScreenshot 方法:
-(void)takeScreenshot {
CGRect contextRect = CGRectMake(0, 0, 768, 1004);
UIGraphicsBeginImageContext(contextRect.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// do something with the viewImage here.
}
This basically moves to the top of the scroll view, and takes a screenshot of the visible area. It works fine when the iPad is oriented portrait, but when it's in landscape the bottom of the image is cut off (as the height of the visible area is only 748, not 1004).
这基本上移动到滚动视图的顶部,并截取可见区域的屏幕截图。当 iPad 面向纵向时它工作正常,但当它处于横向时,图像的底部被切断(因为可见区域的高度仅为 748,而不是 1004)。
Is it possible to get a snapshot of the UIScrollView
, including areas not on screen? Or do I need to scroll the view down, take a second photo and stitch them together?
是否可以获得 的快照UIScrollView
,包括不在屏幕上的区域?或者我是否需要向下滚动视图,拍第二张照片并将它们拼接在一起?
回答by Stefan Arentz
Here is code that works ...
这是有效的代码......
- (IBAction) renderScrollViewToImage
{
UIImage* image = nil;
UIGraphicsBeginImageContext(_scrollView.contentSize);
{
CGPoint savedContentOffset = _scrollView.contentOffset;
CGRect savedFrame = _scrollView.frame;
_scrollView.contentOffset = CGPointZero;
_scrollView.frame = CGRectMake(0, 0, _scrollView.contentSize.width, _scrollView.contentSize.height);
[_scrollView.layer renderInContext: UIGraphicsGetCurrentContext()];
image = UIGraphicsGetImageFromCurrentImageContext();
_scrollView.contentOffset = savedContentOffset;
_scrollView.frame = savedFrame;
}
UIGraphicsEndImageContext();
if (image != nil) {
[UIImagePNGRepresentation(image) writeToFile: @"/tmp/test.png" atomically: YES];
system("open /tmp/test.png");
}
}
The last few lines simply write the image to /tmp/test.png and then opens it in Preview.app. This obviously only works on in the Simulator :-)
最后几行只是将图像写入 /tmp/test.png,然后在 Preview.app 中打开它。这显然只适用于模拟器:-)
Complete project in the ScrollViewScreenShot Github Repository
回答by gleb vodovozov
For me, the currently accepted answer from Stefan Arentzdidn't work.
对我来说,Stefan Arentz 目前接受的答案不起作用。
I had to implement this on iOS 8 and above, and tested on the iPhone. The accepted answer just renders the visible part of a scroll view, while the rest of image remains blank.
我必须在 iOS 8 及更高版本上实现这一点,并在 iPhone 上进行测试。接受的答案只是呈现滚动视图的可见部分,而图像的其余部分保持空白。
I tried fixing this using drawViewHierarchyInRect
- no luck. Depending on afterScreenUpdates
being true
or false
I got stretched part of image or only part of the contents.
我尝试使用修复此问题drawViewHierarchyInRect
- 没有运气。取决于afterScreenUpdates
存在true
或false
我拉伸了图像的一部分或仅部分内容。
The only way I've found to achieve correct snapshotting of a UIScrollView
's entire contents is to add it to another temporary view and then render it.
我发现实现 aUIScrollView
的全部内容的正确快照的唯一方法是将其添加到另一个临时视图,然后呈现它。
Sample code is below (scrollview
is outlet in my VC)
示例代码如下(scrollview
是我 VC 中的插座)
func getImageOfScrollView() -> UIImage {
var image = UIImage()
UIGraphicsBeginImageContextWithOptions(self.scrollView.contentSize, false, UIScreen.mainScreen().scale)
// save initial values
let savedContentOffset = self.scrollView.contentOffset
let savedFrame = self.scrollView.frame
let savedBackgroundColor = self.scrollView.backgroundColor
// reset offset to top left point
self.scrollView.contentOffset = CGPointZero
// set frame to content size
self.scrollView.frame = CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height)
// remove background
self.scrollView.backgroundColor = UIColor.clearColor()
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRectMake(0, 0, self.scrollView.contentSize.width, self.scrollView.contentSize.height))
// save superview
let tempSuperView = self.scrollView.superview
// remove scrollView from old superview
self.scrollView.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.scrollView)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.renderInContext(UIGraphicsGetCurrentContext())
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.scrollView)
// restore saved settings
self.scrollView.contentOffset = savedContentOffset
self.scrollView.frame = savedFrame
self.scrollView.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext()
return image
}
回答by Roopesh Mittal
Working Example of UIView Extension with handling for UIScrollView:
处理 UIScrollView 的 UIView 扩展的工作示例:
extension UIView {
func screenshot() -> UIImage {
if(self is UIScrollView) {
let scrollView = self as! UIScrollView
let savedContentOffset = scrollView.contentOffset
let savedFrame = scrollView.frame
UIGraphicsBeginImageContext(scrollView.contentSize)
scrollView.contentOffset = .zero
self.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
scrollView.contentOffset = savedContentOffset
scrollView.frame = savedFrame
return image!
}
UIGraphicsBeginImageContext(self.bounds.size)
self.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
回答by RyanG
I took this solution from @Roopesh Mittal's answerand made it safer/cleaner.
我从@Roopesh Mittal 的回答中获取了这个解决方案,并使其更安全/更清洁。
Swift 5 compatible
Swift 5 兼容
fileprivate extension UIScrollView {
func screenshot() -> UIImage? {
let savedContentOffset = contentOffset
let savedFrame = frame
UIGraphicsBeginImageContext(contentSize)
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
contentOffset = savedContentOffset
frame = savedFrame
return image
}
}
回答by 0xa6a
A refined Swift 4.x/5.0 version, based on @RyanG 's answer:
一个经过改进的 Swift 4.x/5.0 版本,基于@RyanG 的回答:
fileprivate extension UIScrollView {
func screenshot() -> UIImage? {
// begin image context
UIGraphicsBeginImageContextWithOptions(contentSize, false, 0.0)
// save the orginal offset & frame
let savedContentOffset = contentOffset
let savedFrame = frame
// end ctx, restore offset & frame before returning
defer {
UIGraphicsEndImageContext()
contentOffset = savedContentOffset
frame = savedFrame
}
// change the offset & frame so as to include all content
contentOffset = .zero
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
guard let ctx = UIGraphicsGetCurrentContext() else {
return nil
}
layer.render(in: ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
}
回答by Krishna Kirana
SWIFT 3 version:
SWIFT 3 版本:
func snapshot() -> UIImage?
{
UIGraphicsBeginImageContext(scrollView.contentSize)
let savedContentOffset = scrollView.contentOffset
let savedFrame = scrollView.frame
scrollView.contentOffset = CGPoint.zero
scrollView.frame = CGRect(x: 0, y: 0, width: scrollView.contentSize.width, height: scrollView.contentSize.height)
scrollView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
scrollView.contentOffset = savedContentOffset
scrollView.frame = savedFrame
UIGraphicsEndImageContext()
return image
}
This worked for me
这对我有用
回答by IceFloe
In iOS 13 I have ran into issue that this line won't work
在 iOS 13 中,我遇到了这条线不起作用的问题
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
to fix the issue, I am removing scrollview from parent and then attaching in after taking the screenshot.
为了解决这个问题,我正在从父级中删除滚动视图,然后在截取屏幕截图后附加。
回答by clopex
SWIFT 3 version thanks to @gleb vodovozov:
感谢@gleb vodovozov 的 SWIFT 3 版本:
func getImageOfScrollView()->UIImage{
var image = UIImage();
UIGraphicsBeginImageContextWithOptions(self.scrollView.contentSize, false, UIScreen.main.scale)
// save initial values
let savedContentOffset = self.scrollView.contentOffset;
let savedFrame = self.scrollView.frame;
let savedBackgroundColor = self.scrollView.backgroundColor
// reset offset to top left point
self.scrollView.contentOffset = CGPoint.zero;
// set frame to content size
self.scrollView.frame = CGRect(x: 0, y: 0, width: self.scrollView.contentSize.width, height: self.scrollView.contentSize.height)
// remove background
self.scrollView.backgroundColor = UIColor.clear
// make temp view with scroll view content size
// a workaround for issue when image on ipad was drawn incorrectly
let tempView = UIView(frame: CGRect(x: 0, y: 0, width: self.scrollView.contentSize.width, height: self.scrollView.contentSize.height))
// save superview
let tempSuperView = self.scrollView.superview
// remove scrollView from old superview
self.scrollView.removeFromSuperview()
// and add to tempView
tempView.addSubview(self.scrollView)
// render view
// drawViewHierarchyInRect not working correctly
tempView.layer.render(in: UIGraphicsGetCurrentContext()!)
// and get image
image = UIGraphicsGetImageFromCurrentImageContext()!;
// and return everything back
tempView.subviews[0].removeFromSuperview()
tempSuperView?.addSubview(self.scrollView)
// restore saved settings
self.scrollView.contentOffset = savedContentOffset;
self.scrollView.frame = savedFrame;
self.scrollView.backgroundColor = savedBackgroundColor
UIGraphicsEndImageContext();
return image
}
回答by Moshe
If you don't want to expand your scroll view beyond the entire screen (and it won't work with autolayout anyway) there's a better way.
如果您不想将滚动视图扩展到整个屏幕之外(并且无论如何它都不会与自动布局一起使用),那么有更好的方法。
You can use core graphics transforms in conjunction with the contentOffset
of the scroll view to accomplish the same thing.
您可以将核心图形转换与contentOffset
滚动视图的结合使用来完成相同的事情。
//
// ScrollViewSnapshotter.swift
// ScrollViewSnapshotter
//
// Created by Moshe Berman on 4/10/16.
// Copyright ? 2016 Moshe Berman. All rights reserved.
//
import UIKit
class ScrollViewSnapshotter: NSObject {
func PDFWithScrollView(scrollview: UIScrollView) -> NSData {
/**
* Step 1: The first thing we need is the default origin and size of our pages.
* Since bounds always start at (0, 0) and the scroll view's bounds give us
* the correct size for the visible area, we can just use that.
*
* In the United States, a standard printed page is 8.5 inches by 11 inches,
* but when generating a PDF it's simpler to keep the page size matching the
* visible area of the scroll view. We can let our printer software (such
* as the Preview app on OS X or the Printer app on iOS) do the scaling.
*
* If we wanted to scale ourselves, we could multiply each of those
* numbers by 72, to get the number of points for each dimension.
* We would have to change how we generated the the pages below, so
* for simplicity, we're going to stick to one page per screenful of content.
*/
let pageDimensions = scrollview.bounds
/**
* Step 2: Now we need to know how many pages we will need to fit our content.
* To get this, we divide our scroll views dimensions by the size
* of each page, in either direction.
* We also need to round up, so that the pages don't get clipped.
*/
let pageSize = pageDimensions.size
let totalSize = scrollview.contentSize
let numberOfPagesThatFitHorizontally = Int(ceil(totalSize.width / pageSize.width))
let numberOfPagesThatFitVertically = Int(ceil(totalSize.height / pageSize.height))
/**
* Step 3: Set up a Core Graphics PDF context.
*
* First we create a backing store for the PDF data, then
* pass it and the page dimensions to Core Graphics.
*
* We could pass in some document information here, which mostly cover PDF metadata,
* including author name, creator name (our software) and a password to
* require when viewing the PDF file.
*
* Also note that we can use UIGraphicsBeginPDFContextToFile() instead,
* which writes the PDF to a specified path. I haven't played with it, so
* I don't know if the data is written all at once, or as each page is closed.
*/
let outputData = NSMutableData()
UIGraphicsBeginPDFContextToData(outputData, pageDimensions, nil)
/**
* Step 4: Remember some state for later.
* Then we need to clear the content insets, so that our
* core graphics layer and our content offset match up.
* We don't need to reset the content offset, because that
* happens implicitly, in the loop below.
*/
let savedContentOffset = scrollview.contentOffset
let savedContentInset = scrollview.contentInset
scrollview.contentInset = UIEdgeInsetsZero
/**
* Step 6: Now we loop through the pages and generate the data for each page.
*/
if let context = UIGraphicsGetCurrentContext()
{
for indexHorizontal in 0 ..< numberOfPagesThatFitHorizontally
{
for indexVertical in 0 ..< numberOfPagesThatFitVertically
{
/**
* Step 6a: Start a new page.
*
* This automatically closes the previous page.
* There's a similar method UIGraphicsBeginPDFPageWithInfo,
* which allows you to configure the rectangle of the page and
* other metadata.
*/
UIGraphicsBeginPDFPage()
/**
* Step 6b:The trick here is to move the visible portion of the
* scroll view *and* adjust the core graphics context
* appropriately.
*
* Consider that the viewport of the core graphics context
* is attached to the top of the scroll view's content view
* and we need to push it in the opposite direction as we scroll.
* Further, anything not inside of the visible area of the scroll
* view is clipped, so scrolling will move the core graphics viewport
* out of the rendered area, producing empty pages.
*
* To counter this, we scroll the next screenful into view, and adjust
* the core graphics context. Note that core graphics uses a coordinate
* system which has the y coordinate decreasing as we go from top to bottom.
* This is the opposite of UIKit (although it matches AppKit on OS X.)
*/
let offsetHorizontal = CGFloat(indexHorizontal) * pageSize.width
let offsetVertical = CGFloat(indexVertical) * pageSize.height
scrollview.contentOffset = CGPointMake(offsetHorizontal, offsetVertical)
CGContextTranslateCTM(context, -offsetHorizontal, -offsetVertical) // NOTE: Negative offsets
/**
* Step 6c: Now we are ready to render the page.
*
* There are faster ways to snapshot a view, but this
* is the most straightforward way to render a layer
* into a context.
*/
scrollview.layer.renderInContext(context)
}
}
}
/**
* Step 7: End the document context.
*/
UIGraphicsEndPDFContext()
/**
* Step 8: Restore the scroll view.
*/
scrollview.contentInset = savedContentInset
scrollview.contentOffset = savedContentOffset
/**
* Step 9: Return the data.
* You can write it to a file, or display it the user,
* or even pass it to iOS for sharing.
*/
return outputData
}
}
Here's a blog postI wrote explaining the process.
这是我写的一篇博客文章,解释了这个过程。
The process for generating a PDF is very similar to snapshotting an image, except instead of pages, you'd need to make one large canvas that matches the size of the scroll view and then grab the contents in chunks.
生成 PDF 的过程与快照图像非常相似,不同之处在于您需要制作一个与滚动视图大小相匹配的大画布,然后分块抓取内容,而不是页面。
回答by Adam
Here's another way of doing it, which takes the zoom level into account. I have a scrollview with 4 different UIImageView layers in it, and I want to take a screenshot of their current state:
这是另一种方法,它考虑了缩放级别。我有一个包含 4 个不同 UIImageView 层的滚动视图,我想截取它们当前状态的屏幕截图:
float theScale = 1.0f / theScrollView.zoomScale;
// The viewing rectangle in absolute coordinates
CGRect visibleArea = CGRectMake((int)(theScrollView.contentOffset.x * theScale), (int)(theScrollView.contentOffset.y * theScale),
(int)(theScrollView.bounds.size.width * theScale), (int)(theScrollView.bounds.size.height * theScale));
NSArray *layers = [NSArray arrayWithObjects:imageLayer1, imageLayer2, imageLayer3, imageLayer4, nil];
UIGraphicsBeginImageContext(visibleArea.size);
for (UIImageView *layer in layers) {
CALayer *coreLayer = layer.layer;
coreLayer.bounds = CGRectMake(layer.frame.origin.x - visibleArea.origin.x, layer.frame.origin.y - visibleArea.origin.y, layer.frame.size.width, layer.frame.size.height);
[coreLayer renderInContext:UIGraphicsGetCurrentContext()];
}
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
This takes the screenshot in absolute coordinates. That is, if you have a 2048*2048 image in the scrollview and you can see about a quarter of it, then regardless of the resolution of your screen it would take a screenshot of 512*512. If you want to take a screenshot at your screen resolution (say, 320*480) then you have to adjust the image as follows, directly after the above code:
这以绝对坐标截取屏幕截图。也就是说,如果您在滚动视图中有一个 2048*2048 的图像并且您可以看到它的四分之一,那么无论您的屏幕分辨率如何,它都会截取 512*512 的屏幕截图。如果您想以您的屏幕分辨率(例如 320*480)截取屏幕截图,那么您必须在上面的代码之后直接按如下方式调整图像:
UIGraphicsBeginImageContext(theScrollView.frame.size);
[screenshot drawInRect:CGRectMake(0, 0, theScrollView.frame.size.width, theScrollView.frame.size.height)];
UIImage *smallScreenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();