裁剪后无法绘制阴影

Can't draw shadow after clipping

我需要在用户照片周围画阴影。我通过画一个圆圈然后剪裁上下文来画那些圆圈照片。这是我的代码片段:

+ (UIImage*)roundImage:(UIImage*)img imageView:(UIImageView*)imageView withShadow:(BOOL)shadow
{
    UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, [UIScreen mainScreen].scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextAddEllipseInRect(context, CGRectMake(0,0, imageView.width, imageView.height));
    CGContextSaveGState(context);
    CGContextClip(context);
    [img drawInRect:imageView.bounds];
    CGContextRestoreGState(context);
    if (shadow) {
        CGContextSetShadowWithColor(context, CGSizeMake(0, 0), 5, [kAppColor lighterColor].CGColor);
    }
    CGContextDrawPath(context, kCGPathFill);
    UIImage* roundImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return roundImage;
}

但是在裁剪区域后我无法在下面绘制阴影。所以我不得不在照片后面画一个带阴影的圆圈。

+ (UIImage *)circleShadowFromRect:(CGRect)rect circleDiameter:(CGFloat)circleDiameter shadowColor:(UIColor*)color
{
    UIGraphicsBeginImageContextWithOptions(rect.size, NO, [UIScreen mainScreen].scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
    CGFloat circleStartPointX = CGRectGetMidX(rect) - circleDiameter * 0.5;
    CGFloat circleStartPointY = CGRectGetMidY(rect) - circleDiameter * 0.5;
    CGContextAddEllipseInRect(context, CGRectMake(circleStartPointX,circleStartPointY, circleDiameter, circleDiameter));
    CGContextSetShadowWithColor(context, CGSizeMake(0, 0), 5, color.CGColor);
    CGContextDrawPath(context, kCGPathFill);
    UIImage *circle = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return circle;
}

这种方法的问题显然是它影响了我的应用程序的性能 - 多画了两次圆加上它是 tableview。 我的问题是如何避免在剪切上下文后绘制第二个圆圈和绘制阴影?我确实保存并恢复了状态,但没有帮助,我可能做错了。我还假设 drawInRect 关闭了当前路径,这就是为什么阴影不知道在哪里绘制自己。我是否应该再次调用 CGContextAddEllipseInRect 然后绘制阴影?

关于性能

性能很有趣,但通常很不直观。

开发人员,包括我自己,通常都知道什么更快或更有效,但这很少那么简单。实际上,它是在 CPU、GPU、内存消耗、代码复杂度等方面进行权衡。

任何 "poorly performing code" 都会在上述其中一项中遇到瓶颈,并且任何不针对该瓶颈的改进都不会 "improve performance" 该代码。哪一个最终成为瓶颈将因情况而异,甚至可能因设备而异。即使拥有丰富的经验,也很难预测瓶颈是什么因素。唯一可以确定的方法是在每次更改之前和之后跨真实设备进行测量(阅读:使用 Instruments)。

在同一图像中绘制阴影

也就是说,您可以更改上面的代码以在与圆形图像相同的图像中绘制阴影,但它必须在裁剪之前发生。

下面是您的代码的 Swift 版本,它就是这样做的。

func roundedImage(for image: UIImage, bounds: CGRect, withShadow shadow: Bool) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
    defer {
        UIGraphicsEndImageContext() 
    }

    let context = UIGraphicsGetCurrentContext()
    let circle = CGPathCreateWithEllipseInRect(bounds, nil)

    if shadow {
        // draw an elliptical shadow
        CGContextSaveGState(context)

        CGContextSetShadowWithColor(context, .zero, 5.0, UIColor.blackColor().CGColor)
        CGContextAddPath(context, circle)
        CGContextFillPath(context)

        CGContextRestoreGState(context) 
    }

    // clip to an elliptical shape, and draw the image
    CGContextAddPath(context, circle)
    CGContextClip(context)
    image.drawInRect(bounds)

    return UIGraphicsGetImageFromCurrentImageContext()
}

一些注意事项:

  • 为比例因子传递 0.0 会导致设备主屏幕的比例因子。
  • 在绘制阴影时保存和恢复上下文,这样绘制图像时就不会重新计算阴影。

此代码不会扩展图像的大小以解决阴影问题。您可能只想在绘制图像时扩展尺寸(导致有阴影和没有阴影的图像尺寸不同),或者总是扩展尺寸(导致没有阴影的图像周围为空 space)。您可以选择最适合您的行为。

要考虑的替代方案

这更像是关于可能的替代方案及其假设的性能差异的有趣推测。这些建议并不是要严格采纳,更多的是为了说明没有单一的"correct"解决方案。

绘制的阴影始终相同,因此您可以通过仅绘制一次阴影图像然后重新使用它来假设用 CPU 周期换取内存。更进一步,您甚至可以为阴影包含一个资产(以更大的捆绑包和从磁盘读取时间为代价),这样您甚至不必在第一次绘制它。

带有阴影的图像仍然是透明的,这意味着它必须与背景混合(注意:混合几乎从来不是当今硬件的问题。这更像是假设。).您可以将背景颜色参数传递给函数并让它生成不透明图像。

裁剪图片需要付费。如果生成的图像是不透明的,它可能包含一个带有背景、圆圈和预渲染的圆形切口的资产(下方渲染在橙色背景之上)。通过这种方式,可以在不裁剪的情况下将个人资料图像绘制到图像上下文中,并且将在其上方绘制阴影图像。

混合仍在 CPU 上进行。通过在上面添加带有预渲染阴影和背景剪切的第二层,然后混合工作可以从 CPU 转移到 GPU。

等等...


另一个方向是图层配置。您可以使用图层的角半径对图像进行圆角处理,并使用各种阴影属性绘制阴影。只要您记得明确指定 shadowPath ,性能差异应该很小。

您可以通过分析 两种备选方案并在真实设备上发布版本来验证最后的陈述。 ;)