获取UIScrollView的屏幕截图,包括屏幕外的部分

59

我有一个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.
}

这基本上是将滚动视图移动到顶部,并截取可见区域的屏幕截图。当iPad竖向时,它可以正常工作,但在横向时,图像底部被切断了(因为可见区域的高度只有748,而不是1004)。

是否可能获取UIScrollView的整个快照,包括不在屏幕上的区域?或者我需要向下滚动视图,拍第二张照片再将它们拼接在一起?

16个回答

113

这里是可用的代码...

- (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");
    }
}

最后几行代码将图像写入 /tmp/test.png 并在 Preview.app 中打开它。这显然只适用于模拟器 :-)

ScrollViewScreenShot Github 仓库中的完整项目。


6
赞成缩进技巧,从未想过可以这样使用花括号。 - Dan Abramov
使用Xcode 5模拟器时,截图保存在哪个位置?应用程序的/tmp文件夹为空! - Eric
4
很好的解决方案,但我使用了UIGraphicsBeginImageContextWithOptions(_scrollView.contentSize, NO, 0.0)以获得更高的分辨率。 - Francis F
8
在iOS 13设备上使用该方法,屏幕外部分是黑色的。似乎在先前的操作系统版本中可以正常运行,但在iOS 13上不行? - Alen Liang
2
@无夜之星辰 是的,我创建了视图 但没有将其附加到任何父视图 ,然后截屏。 代码类似于 scrollView.frame = CGRect(x: 0, y: 0, width: 宽度, height: 高度) 并通过创建一个 UIGraphicsImageRenderer(size: CGSize(width:宽度, height:高度) 来截取视图。 - Alen Liang
显示剩余17条评论

26

对我而言,目前由Stefan Arentz提供的解答并没有起作用。

我需要在iOS 8及以上版本上实现这个功能,并在iPhone上测试。这个被接受的答案只会呈现可见部分的滚动视图,而其余部分则是空白的。

我尝试使用drawViewHierarchyInRect进行修复 - 没有成功。根据afterScreenUpdatestrue还是false,我得到了拉伸的图像部分或者只有一部分内容的情况。

我发现唯一正确地截取UIScrollView整个内容的方法是将它添加到另一个临时视图中,然后渲染它。

以下是示例代码(scrollview是我的VC中的outlet):

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
}

3
好的,图像已经创建了,但不幸的是我的滚动视图在父视图中没有被还原,有什么建议吗? - Ettore Gallina
1
与@Ettore提到的问题相同。 - Anjali jariwala
同样的问题。不幸的是,我的滚动视图无法恢复。@Sam 你能帮我解决一下吗? - Shiv Prakash
图片已保存,但我的scrollView已被移除。 - 无夜之星辰

10

使用UIView扩展的工作示例,包括处理UIScrollView:

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!

        }
}

1
这个很好用——我也在这里发布了一个更干净/更安全的解决方案,基于这个。 - RyanG
1
只需使用UIGraphicsBeginImageContextWithOptions(size, false, 0)以获得更好的质量... ;) - M.R
1
iOS 13上,当scrollView大于屏幕时无法工作。 - 无夜之星辰

9

2023 更新:Deepak Deore 的回答似乎是最新和最好的方法。它可以捕捉 iOS 13 中屏幕外的内容,而无需删除/重新添加滚动视图。


我从@Roopesh Mittal的答案中采用了这个解决方案,并使其更加安全/清洁。
适用于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
    }
}

在某些情况下,framecontentOffset不会被恢复,并且UIGraphicsEndImageContext不会被调用。 - wzso
我在使用这段代码时没有遇到过这种情况,但我相信它可能以某种方式发生。无论如何,在某些情况下(比如我的情况),保护和不崩溃可能更加理想。 - RyanG
1
只需在上下文声明之前添加这两行代码即可获得更高质量的图像。let scale = UIScreen.main.scale UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale); - Vijay Kharage

8

基于 @RyanG 的答案,经过精细处理的 Swift 4.x/5.0 版本:

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
    }
}

3
在iOS 13上,这对我没有效果 :( 图像的大小是整个滚动视图的大小,但只有屏幕上可见的部分被呈现。 - lewis
@lewis 我遇到了同样的问题,你解决了吗? - 无夜之星辰
此外,还遇到了 iOS 13 的问题。 - tennis779
@lewis,你找到这个问题的答案了吗?我找不到它。 - Rogelio Heredia
@无夜之星辰 抱歉,我记不得了 :( 但是我确实看到我给这个答案 https://dev59.com/BnA65IYBdhLWcg3w-jum#58594935 点了赞 - lewis
@RogelioHeredia 抱歉,我记不得了 :( 但是我确实看到我点赞了这个答案 https://dev59.com/BnA65IYBdhLWcg3w-jum#58594935 - lewis

8

在iOS 13中,我遇到了这个问题:这一行无法工作:

frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)

为了解决这个问题,我将滚动视图从其父视图中移除,然后在截屏后重新添加。

完整代码:

func screenshot() -> UIImage? {
        let savedContentOffset = contentOffset
        let savedFrame = frame
        defer {
            contentOffset = savedContentOffset
            frame = savedFrame
        }

        contentOffset = .zero
        frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)

        let image = UIGraphicsImageRenderer(bounds: CGRect(origin: .zero, size: contentSize)).image { renderer in
            let context = renderer.cgContext
            layer.render(in: context)
        }

        return image
    }

获取屏幕截图:

func getScreenshot() -> UIImage? {
     scrollView.removeFromSuperview()
     let image = scrollView.screenshot()
     addScrollView()
     return image
}

从父视图中移除ScrollView!就是这样! - Alen Liang
你能详细说明/分享一下实现这个的完整代码吗?我遇到了一个问题,截图被成功捕获,但scrollView(或者在我的情况下是tableView)消失了。 - vikzilla
@vikzilla 已为您更新 - IceFloe
@IceFloe 谢谢您的更新,但是解决方案没有包括实例化 "contentOffest" 和 "frame" 的位置。而且它也没有展示任何关于 addScrollView() 的代码。 - vikzilla
@vikzilla contentOffest和frame是UIScrollView中的属性。addScrollView将取决于您如何将scrollView添加到主视图中。 - IceFloe

4

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
}

这对我有帮助。

在 Swift 4 中也完美运行。对这个答案点赞。愉快编码! - Prashant Gaikwad
6
几乎可以工作,问题在于滚动视图在屏幕高度之后呈现为空白区域。我该如何解决? - Hilalkah

4

许多人指出,当前的解决方案不起作用,而其他的解决方案建议从超级视图中删除滚动视图,这会导致失去所有的约束。

在这里,我暂时禁用与滚动视图相关的所有约束,并在截取屏幕快照后再将它们打开:

extension UIScrollView {
    func screenshot() -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(contentSize, false, 0.0)
        // save the orginal offset, take a ref to all constraints related to the view
        let savedContentOffset = contentOffset
        let actualConstraints = relatedConstraints()
        // deactivate non needed constraints so they won't stop us from resiging scroll view
        NSLayoutConstraint.deactivate(actualConstraints)
        // enable auth generated constraints based on the frame
        translatesAutoresizingMaskIntoConstraints = true
        
        frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
        contentOffset = .zero
        defer {
            UIGraphicsEndImageContext()
            
            // reset original constraints
            translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate(actualConstraints)
            
            // layout superview needed before resetting content offset
            superview?.layoutIfNeeded()
            contentOffset = savedContentOffset
        }
        guard let ctx = UIGraphicsGetCurrentContext() else {
            return nil
        }
        layer.render(in: ctx)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        return image
    }
}

extension UIView {
    func relatedConstraints() -> [NSLayoutConstraint] {
        var constraints = self.constraints
        var parent = superview
        while parent != nil {
            constraints.append(contentsOf: parent!.constraints.filter { $0.firstItem === self || $0.secondItem === self })
            parent = parent!.superview
        }
        return constraints
    }
}

3

感谢 @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
}

2

这里有另一种方法,它考虑了缩放级别。我有一个带有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();

这将在绝对坐标中进行屏幕截图。也就是说,如果您在滚动视图中有一个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();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接