如何在Retina显示屏上不损失质量地捕获UIView为UIImage

312

我的代码在普通设备上运行良好,但在视网膜设备上会生成模糊的图像。

有没有人知道我问题的解决方案?

+ (UIImage *) imageWithView:(UIView *)view
{
    UIGraphicsBeginImageContext(view.bounds.size);
    [view.layer renderInContext:UIGraphicsGetCurrentContext()];

    UIImage * img = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return img;
}

模糊。在我看来,正确的比例似乎丢失了... - Daniel
我也是。遇到了同样的问题。 - RainCast
你在哪里显示结果图像?正如其他人所解释的那样,我认为您正在将视图渲染到比例为1的图像上,而您的设备具有比例为2或3。因此,您正在将其降低到较低的分辨率。这是一种质量损失,但不应该模糊。但是,当您再次查看此图像(在同一屏幕上?)在具有比例为2的设备上时,它将从低分辨率转换为更高分辨率,使用每个截图像素的2个像素。这种放大最可能使用插值(iOS上的默认值),平均颜色值以获取额外像素。 - Hans Terje Bakke
18个回答

679

切换使用 UIGraphicsBeginImageContextWithOptions(在此页面中有记录)。将缩放参数(即第三个参数)设置为 0.0,您将获得一个与屏幕缩放因子相同的上下文。

UIGraphicsBeginImageContext 使用固定的缩放因子 1.0,因此,在 iPhone 4 上与其他 iPhone 上获得完全相同的图像。我敢打赌,要么 iPhone 4 在隐式缩放时应用了过滤器,要么你的大脑会察觉到它比周围的一切都不太清晰。

所以,我猜:

#import <QuartzCore/QuartzCore.h>

+ (UIImage *)imageWithView:(UIView *)view
{
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0);
    [view.layer renderInContext:UIGraphicsGetCurrentContext()];

    UIImage * img = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return img;
}

在 Swift 4 中:

func image(with view: UIView) -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
    defer { UIGraphicsEndImageContext() }
    if let context = UIGraphicsGetCurrentContext() {
        view.layer.render(in: context)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        return image
    }
    return nil
}

7
汤米的回答是正确的,但您仍需导入#import <QuartzCore/QuartzCore.h>以消除renderInContext:警告。 - gwdp
2
@WayneLiu 你可以明确指定一个缩放因子为2.0,但你可能不会得到完全与iPhone 4质量相同的图像,因为任何已加载的位图都是标准版本而不是@2x版本。我认为这没有解决办法,因为没有办法指示每个当前持有UIImage的人重新加载但强制使用高分辨率版本(而且由于预Retina设备中可用的RAM较少,你可能会遇到问题)。 - Tommy
6
使用0.0f作为比例参数不如使用[[UIScreen mainScreen] scale]更合适,后者同样有效。 - Adam Carter
21
“比例尺度(scale)"是应用于位图(bitmap)的缩放因子。如果您指定值为0.0,则缩放因子将设置为设备主屏幕的缩放因子。” 这是明确记录的,因此在我看来,使用0.0f更简单,更好。 - cprcrack
5
这个回答对于iOS 7已经过时了。请查看我的答案,了解新的“最佳”方法来完成这个任务。 - Dima
显示剩余11条评论

239

目前被接受的答案已过时,至少在支持iOS 7的情况下是这样。

以下是您应该使用的内容,如果您仅支持iOS7+:

+ (UIImage *) imageWithView:(UIView *)view
{
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0f);
    [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:NO];
    UIImage * snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return snapshotImage;
}

Swift 4:

func imageWithView(view: UIView) -> UIImage? {
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
    defer { UIGraphicsEndImageContext() }
    view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
    return UIGraphicsGetImageFromCurrentImageContext()
}
根据这篇文章,你可以看到新的iOS7方法drawViewHierarchyInRect:afterScreenUpdates:renderInContext:快很多。 benchmark

5
毫无疑问,drawViewHierarchyInRect:afterScreenUpdates: 更快。我刚在 Instruments 中运行了时间分析器。我的图片生成时间从 138 毫秒缩短到了 27 毫秒。 - ThomasCle
9
由于某些原因,当我尝试为Google Maps标记创建自定义图标时,它对我没有起作用;我只得到了黑色矩形 :( - JakeP
6
@CarlosP,你尝试过将afterScreenUpdates:设置为YES吗? - Dima
7
将 afterScreenUpdates 属性设置为 YES 可以解决我遇到的黑色矩形问题。 - hyouuu
4
我也收到了黑色图片。原来如果我在viewDidLoadviewWillAppear:中调用代码,图片就会变成黑色;我必须在viewDidAppear:中这样做。所以,我最终还是回归了renderInContext:方法。 - manicaesar
显示剩余8条评论

32

我基于@Dima的解决方案创建了一个Swift扩展:

extension UIImage {
    class func imageWithView(view: UIView) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.opaque, 0.0)
        view.drawViewHierarchyInRect(view.bounds, afterScreenUpdates: true)
        let img = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return img
    }
}

编辑:Swift 4 改进版

extension UIImage {
    class func imageWithView(_ view: UIView) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0)
        defer { UIGraphicsEndImageContext() }
        view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
        return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
    }
}

用法:

let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))  
let image = UIImage.imageWithView(view)

2
谢谢你的想法!顺便说一下,你也可以在开始上下文后立即推迟 { UIGraphicsEndImageContext() },避免引入本地变量 img ;) - Dennis L
非常感谢您提供详细的解释和用法! - dbrownjave
为什么这是一个接受视图的class函数? - Rudolf Adamkovič
这个函数的工作方式类似于static函数,但由于没有覆盖的需要,您可以将其简单地更改为static函数而不是class - Heberti Almeida

31

iOS Swift

使用现代的UIGraphicsImageRenderer

public extension UIView {
    @available(iOS 10.0, *)
    public func renderToImage(afterScreenUpdates: Bool = false) -> UIImage {
        let rendererFormat = UIGraphicsImageRendererFormat.default()
        rendererFormat.opaque = isOpaque
        let renderer = UIGraphicsImageRenderer(size: bounds.size, format: rendererFormat)

        let snapshotImage = renderer.image { _ in
            drawHierarchy(in: bounds, afterScreenUpdates: afterScreenUpdates)
        }
        return snapshotImage
    }
}

@masaldana2 或许现在还没有,但是一旦他们开始弃用某些东西或者添加硬件加速渲染或改进,你最好站在巨人的肩膀上(也就是使用最新的苹果API)。 - Juan Boero
有没有人知道这个“renderer”和旧的“drawHierarchy”相比性能如何? - Nat

24

为了改进@Tommy和@Dima的答案,使用以下类别将UIView渲染为UIImage具有透明背景并且不失去质量。在iOS7上运行。(或者只需在实现中重用该方法,将self引用替换为您的图像)

UIView+RenderViewToImage.h

#import <UIKit/UIKit.h>

@interface UIView (RenderToImage)

- (UIImage *)imageByRenderingView;

@end

UIView+RenderViewToImage.m

#import "UIView+RenderViewToImage.h"

@implementation UIView (RenderViewToImage)

- (UIImage *)imageByRenderingView
{
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
    [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
    UIImage * snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return snapshotImage;
}

@end

1
如果我使用带有afterScreenUpdates:YES的drawViewHierarchyInRect,我的图像纵横比会改变并且图像会变形。 - Michael
当您的UIView内容更改时,是否会发生这种情况?如果是,则尝试重新生成此UIImage。很抱歉我无法自己测试,因为我在手机上。 - Glogo
我有同样的问题,似乎没有人注意到这个问题,你找到了解决这个问题的方法吗?@confile? - Reza.Ab
这正是我需要的,但它没有起作用。我尝试将@interface@implementation都设为(RenderViewToImage),并在ViewController.h的头文件中添加#import "UIView+RenderViewToImage.h",所以这是正确的用法吗?例如:UIImageView *test = [[UIImageView alloc] initWithImage:[self imageByRenderingView]]; [self addSubview:test]; - Greg
以下是在 ViewController.m 中尝试渲染的视图所使用的方法 - (UIView *)testView { UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 50, 50)]; v1.backgroundColor = [UIColor redColor]; UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 150, 150)]; v2.backgroundColor = [UIColor blueColor]; [v2 addSubview:v1]; return v2; } - Greg

15

Swift 3

基于Dima的回答,使用UIView扩展的Swift 3解决方案应该像这样:

extension UIView {
    public func getSnapshotImage() -> UIImage {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0)
        self.drawHierarchy(in: self.bounds, afterScreenUpdates: false)
        let snapshotImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return snapshotImage
    }
}

9

对于 Swift 5.1,您可以使用以下扩展:

extension UIView {

    func asImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)

        return renderer.image { layer.render(in: $0.cgContext) }
    }
}

6

Drop-in Swift 3.0扩展,支持新的iOS 10.0 API和先前的方法。

注意:

  • iOS版本检查
  • 请注意使用defer来简化上下文清理。
  • 还将应用视图的不透明度和当前比例。
  • 没有使用!解包任何东西,这可能会导致崩溃。

extension UIView
{
    public func renderToImage(afterScreenUpdates: Bool = false) -> UIImage?
    {
        if #available(iOS 10.0, *)
        {
            let rendererFormat = UIGraphicsImageRendererFormat.default()
            rendererFormat.scale = self.layer.contentsScale
            rendererFormat.opaque = self.isOpaque
            let renderer = UIGraphicsImageRenderer(size: self.bounds.size, format: rendererFormat)

            return
                renderer.image
                {
                    _ in

                    self.drawHierarchy(in: self.bounds, afterScreenUpdates: afterScreenUpdates)
                }
        }
        else
        {
            UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, self.layer.contentsScale)
            defer
            {
                UIGraphicsEndImageContext()
            }

            self.drawHierarchy(in: self.bounds, afterScreenUpdates: afterScreenUpdates)

            return UIGraphicsGetImageFromCurrentImageContext()
        }
    }
}

5

Swift 2.0:

使用扩展方法:

extension UIImage{

   class func renderUIViewToImage(viewToBeRendered:UIView?) -> UIImage
   {
       UIGraphicsBeginImageContextWithOptions((viewToBeRendered?.bounds.size)!, false, 0.0)
       viewToBeRendered!.drawViewHierarchyInRect(viewToBeRendered!.bounds, afterScreenUpdates: true)
       viewToBeRendered!.layer.renderInContext(UIGraphicsGetCurrentContext()!)

       let finalImage = UIGraphicsGetImageFromCurrentImageContext()
       UIGraphicsEndImageContext()

       return finalImage
   }

}

使用方法:

override func viewDidLoad() {
    super.viewDidLoad()

    //Sample View To Self.view
    let sampleView = UIView(frame: CGRectMake(100,100,200,200))
    sampleView.backgroundColor =  UIColor(patternImage: UIImage(named: "ic_120x120")!)
    self.view.addSubview(sampleView)    

    //ImageView With Image
    let sampleImageView = UIImageView(frame: CGRectMake(100,400,200,200))

    //sampleView is rendered to sampleImage
    var sampleImage = UIImage.renderUIViewToImage(sampleView)

    sampleImageView.image = sampleImage
    self.view.addSubview(sampleImageView)

 }

3
这是一个基于@Dima的答案的Swift 4 UIView扩展

extension UIView {
   func snapshotImage() -> UIImage? {
       UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
       drawHierarchy(in: bounds, afterScreenUpdates: false)
       let image = UIGraphicsGetImageFromCurrentImageContext()
       UIGraphicsEndImageContext()
       return image
   }
}

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