在UIImageView上添加一个圆形遮罩层

30
我正在构建一个照片滤镜应用程序(类似Instagram、Camera+等),我的主屏幕是一个 UIImageView,用于向用户呈现图像,并带有一些滤镜和其他选项的底部栏。
其中一个选项是模糊,用户可以使用手指捏合或移动表示非模糊部分(半径和位置)的圆形 - 此圆形外的所有像素都将被模糊。
当用户触摸屏幕时,我想在我的图像上方添加一个半透明层,代表已模糊的部分,具有完全透明的圆,代表未模糊的部分。
所以我的问题是,如何添加这个层?我想需要在我的图像视图上面使用一些视图,并使用一些蒙版来获取我的圆形形状?在这里,我真的很感激一个好的提示。
还有另外一件事
我需要圆不是直接剪切,而是有一种渐变淡出效果。 类似于Instagram:
enter image description here 而且非常重要的是以良好的性能获得此效果,我成功地使用 drawRect: 来获得此效果,但旧设备(iPhone 4、iPod)的性能非常差。

你觉得在UIImageView上面使用可缩放的UIScrollView怎么样?这样,你可以添加一个静态的遮罩(一个中间有透明“孔”的图像),它会平滑地响应用户的手势。 - Stavash
请创建一个带有自定义形状孔的图层蒙版。 - InvalidReferenceException
这是你情况下最好的答案...根据你的需求进行更改...https://dev59.com/82Yq5IYBdhLWcg3w5Um8...我可以在我的情况下使用这段代码.. :) 谢谢 :) - user2289379
3个回答

97

锐化掩模

如果您想要绘制一个由形状(或一系列形状)组成的路径,作为另一个形状中的孔,关键几乎总是使用“奇偶环绕规则”。

来自Cocoa绘图指南环绕规则部分:

环绕规则只是一种算法,用于跟踪组成路径整体填充区域的每个连续区域的信息。从给定区域内的点到任何在路径边界外的点绘制射线,然后使用确定区域是否应该填充的规则解释穿过的路径线的总数(包括隐式线)和每条路径线的方向。

我明白仅有描述可能不是很有帮助,因为缺少上下文和图表解释,所以我建议您阅读我提供的链接。为了创建我们的圆形遮罩层,以下图表描述了甚至/奇偶填充规则允许我们完成的内容:

非零填充规则

Non Zero Winding Rule

奇偶数填充规则

Even Odd Winding Rule

现在,我们只需要使用CAShapeLayer创建半透明遮罩,用户可以通过交互重新定位、扩大和收缩。

代码

#import <QuartzCore/QuartzCore.h>


@interface ViewController ()
@property (strong, nonatomic) IBOutlet UIImageView *imageView;
@property (strong) CAShapeLayer *blurFilterMask;
@property (assign) CGPoint blurFilterOrigin;
@property (assign) CGFloat blurFilterDiameter;
@end


@implementation ViewController

// begin the blur masking operation.
- (void)beginBlurMasking
{
    self.blurFilterOrigin = self.imageView.center;
    self.blurFilterDiameter = MIN(CGRectGetWidth(self.imageView.bounds), CGRectGetHeight(self.imageView.bounds));

    CAShapeLayer *blurFilterMask = [CAShapeLayer layer];
    // Disable implicit animations for the blur filter mask's path property.
    blurFilterMask.actions = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNull null], @"path", nil];
    blurFilterMask.fillColor = [UIColor blackColor].CGColor;
    blurFilterMask.fillRule = kCAFillRuleEvenOdd;
    blurFilterMask.frame = self.imageView.bounds;
    blurFilterMask.opacity = 0.5f;
    self.blurFilterMask = blurFilterMask;
    [self refreshBlurMask];
    [self.imageView.layer addSublayer:blurFilterMask];

    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    [self.imageView addGestureRecognizer:tapGesture];

    UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
    [self.imageView addGestureRecognizer:pinchGesture];
}

// Move the origin of the blur mask to the location of the tap.
- (void)handleTap:(UITapGestureRecognizer *)sender
{
    self.blurFilterOrigin = [sender locationInView:self.imageView];
    [self refreshBlurMask];
}

// Expand and contract the clear region of the blur mask.
- (void)handlePinch:(UIPinchGestureRecognizer *)sender
{
    // Use some combination of sender.scale and sender.velocity to determine the rate at which you want the circle to expand/contract.
    self.blurFilterDiameter += sender.velocity;
    [self refreshBlurMask];
}

// Update the blur mask within the UI.
- (void)refreshBlurMask
{
    CGFloat blurFilterRadius = self.blurFilterDiameter * 0.5f;

    CGMutablePathRef blurRegionPath = CGPathCreateMutable();
    CGPathAddRect(blurRegionPath, NULL, self.imageView.bounds);
    CGPathAddEllipseInRect(blurRegionPath, NULL, CGRectMake(self.blurFilterOrigin.x - blurFilterRadius, self.blurFilterOrigin.y - blurFilterRadius, self.blurFilterDiameter, self.blurFilterDiameter));

    self.blurFilterMask.path = blurRegionPath;

    CGPathRelease(blurRegionPath);
}

...

Code Conventions Diagram

(这个图表可能有助于理解代码中的命名约定)

渐变蒙版

Quartz 2D编程指南渐变部分详细介绍了如何绘制径向渐变,我们可以使用它来创建具有羽化边缘的蒙版。这涉及通过子类化或实现其绘图委托直接绘制CALayer的内容。在这里,我们对其进行子类化以封装与其相关的数据,即起点和直径。

代码

BlurFilterMask.h

#import <QuartzCore/QuartzCore.h>

@interface BlurFilterMask : CALayer
@property (assign) CGPoint origin;      // The centre of the blur filter mask.
@property (assign) CGFloat diameter;    // the diameter of the clear region of the blur filter mask.
@end

BlurFilterMask.m

#import "BlurFilterMask.h"

// The width in points the gradated region of the blur filter mask will span over.
CGFloat const GRADIENT_WIDTH = 50.0f;

@implementation BlurFilterMask

- (void)drawInContext:(CGContextRef)context
{
    CGFloat clearRegionRadius = self.diameter * 0.5f;
    CGFloat blurRegionRadius = clearRegionRadius + GRADIENT_WIDTH;

    CGColorSpaceRef baseColorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat colours[8] = { 0.0f, 0.0f, 0.0f, 0.0f,     // Clear region colour.
                            0.0f, 0.0f, 0.0f, 0.5f };   // Blur region colour.
    CGFloat colourLocations[2] = { 0.0f, 0.4f };
    CGGradientRef gradient = CGGradientCreateWithColorComponents (baseColorSpace, colours, colourLocations, 2);

    CGContextDrawRadialGradient(context, gradient, self.origin, clearRegionRadius, self.origin, blurRegionRadius, kCGGradientDrawsAfterEndLocation);

    CGColorSpaceRelease(baseColorSpace);
    CGGradientRelease(gradient);
}

@end

ViewController.m(无论您在何处实现模糊过滤器遮罩功能)

#import "ViewController.h"
#import "BlurFilterMask.h"
#import <QuartzCore/QuartzCore.h>

@interface ViewController ()
@property (strong, nonatomic) IBOutlet UIImageView *imageView;
@property (strong) BlurFilterMask *blurFilterMask;
@end


@implementation ViewController

// Begin the blur filter masking operation.
- (void)beginBlurMasking
{
    BlurFilterMask *blurFilterMask = [BlurFilterMask layer];
    blurFilterMask.diameter = MIN(CGRectGetWidth(self.imageView.bounds), CGRectGetHeight(self.imageView.bounds));
    blurFilterMask.frame = self.imageView.bounds;
    blurFilterMask.origin = self.imageView.center;
    blurFilterMask.shouldRasterize = YES;
    [self.imageView.layer addSublayer:blurFilterMask];
    [blurFilterMask setNeedsDisplay];

    self.blurFilterMask = blurFilterMask;

    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    [self.imageView addGestureRecognizer:tapGesture];

    UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
    [self.imageView addGestureRecognizer:pinchGesture];
}

// Move the origin of the blur mask to the location of the tap.
- (void)handleTap:(UITapGestureRecognizer *)sender
{
    self.blurFilterMask.origin = [sender locationInView:self.imageView];
    [self.blurFilterMask setNeedsDisplay];
}

// Expand and contract the clear region of the blur mask.
- (void)handlePinch:(UIPinchGestureRecognizer *)sender
{
    // Use some combination of sender.scale and sender.velocity to determine the rate at which you want the mask to expand/contract.
    self.blurFilterMask.diameter += sender.velocity;
    [self.blurFilterMask setNeedsDisplay];
}

...

Code Conventions Diagram

(这个图表可能有助于理解代码中的命名约定)

注意

确保承载图像的UIImageViewmultipleTouchEnabled属性设置为YES/true

multipleTouchEnabled


注意

为了清晰地回答问题,本答案继续使用最初使用的命名约定。这可能会对他人造成轻微的误导。在这种情况下,“掩码”不是指图像掩码,而是更一般意义上的掩码。本答案不使用任何图像掩码操作。


毫无疑问,您已经看到了我的上一个答案,我对它并不满意,我知道这不是最佳解决方案。这个解决方案不仅更清晰,而且性能更好(我对两个解决方案进行了剖析以确保)。 - Elliott
嗨,Elliott,非常感谢你详细的回答 :) 我还没有测试过性能,但它肯定是一个很好的答案。我更新了问题,并提到了另一件事情,如果你能看一下就太好了。 - Eyal
好的,谢谢。我会等待你的回答。你觉得我应该使用资源(.png文件)来实现这个效果吗? - Eyal
@Eyal 使用图像可以很方便地应用复杂形状的遮罩。由于我们需要的遮罩是一个简单的圆形,并且需要根据触摸事件修改其大小和位置,因此我认为最好在代码中动态创建遮罩。 - Elliott
@Eyal 抱歉回复晚了,最近很忙。我已经更新了我的答案,包括使用羽化/渐变遮罩边缘的解决方案。 - Elliott
嗨@ElliottJamesPerry,有没有办法只在圆形遮罩周围添加边框?当我添加描边或图层边框时,它也会在整个图层周围添加(矩形边界)。 - royherma

1

如果有人需要的话,这是Swift代码(还添加了平移手势):

BlurFilterMask.swift

import Foundation
import QuartzCore

class BlurFilterMask : CALayer {

    private let GRADIENT_WIDTH : CGFloat = 50.0

    var origin : CGPoint?
    var diameter : CGFloat?

    override init() {
        super.init()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func drawInContext(ctx: CGContext) {
        let clearRegionRadius : CGFloat  = self.diameter! * 0.5
        let blurRegionRadius : CGFloat  = clearRegionRadius + GRADIENT_WIDTH

        let baseColorSpace = CGColorSpaceCreateDeviceRGB();
        let colours : [CGFloat] = [0.0, 0.0, 0.0, 0.0,     // Clear region
            0.0, 0.0, 0.0, 0.5] // blur region color
        let colourLocations : [CGFloat] = [0.0, 0.4]
        let gradient = CGGradientCreateWithColorComponents (baseColorSpace, colours, colourLocations, 2)


        CGContextDrawRadialGradient(ctx, gradient, self.origin!, clearRegionRadius, self.origin!, blurRegionRadius, .DrawsAfterEndLocation);

    }

}

ViewController.swift

func addMaskOverlay(){
    imageView!.userInteractionEnabled = true
    imageView!.multipleTouchEnabled = true

    let blurFilterMask = BlurFilterMask()

    blurFilterMask.diameter = min(CGRectGetWidth(self.imageView!.bounds), CGRectGetHeight(self.imageView!.bounds))
    blurFilterMask.frame = self.imageView!.bounds
    blurFilterMask.origin = self.imageView!.center
    blurFilterMask.shouldRasterize = true

    self.imageView!.layer.addSublayer(blurFilterMask)

    self.blurFilterMask = blurFilterMask
    self.blurFilterMask!.setNeedsDisplay()

    self.imageView!.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: "handlePinch:"))
    self.imageView!.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "handleTap:"))
    self.imageView!.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "handlePan:"))
}

func donePressed(){
    //save photo and add to textview
    let parent : LoggedInContainerViewController? = self.parentViewController as? LoggedInContainerViewController
    let vc : OrderFlowCareInstructionsTextViewController = parent?.viewControllers[(parent?.viewControllers.count)!-2] as! OrderFlowCareInstructionsTextViewController
    vc.addImageToTextView(imageView?.image)
    parent?.popViewController()
}

//MARK: Mask Overlay
func handleTap(sender : UITapGestureRecognizer){
    self.blurFilterMask!.origin = sender.locationInView(self.imageView!)
    self.blurFilterMask!.setNeedsDisplay()
}

func handlePinch(sender : UIPinchGestureRecognizer){
    self.blurFilterMask!.diameter = self.blurFilterMask!.diameter! + sender.velocity*3
    self.blurFilterMask!.setNeedsDisplay()
}

func handlePan(sender : UIPanGestureRecognizer){

    let translation = sender.translationInView(self.imageView!)
    let center = CGPoint(x:self.imageView!.center.x + translation.x,
        y:self.imageView!.center.y + translation.y)
    self.blurFilterMask!.origin = center
    self.blurFilterMask!.setNeedsDisplay()
}

你好,@royherma,你有可用的Xcode项目吗?我在ViewController.swift中遇到了错误。 - iPsych

1
听起来你想使用包含在GPUImage框架中的GPUImageGaussianSelectiveBlurFilter。这应该是实现你想要的更快、更有效的方法。
你可以将excludeCircleRadius属性与UIPinchGestureRecognizer连接起来,以允许用户更改非模糊圆的大小。然后使用“excludeCirclePoint”属性与UIPanGestureRecognizer一起,允许用户移动非模糊圆的中心。
在此处阅读有关如何应用滤镜的更多信息:

https://github.com/BradLarson/GPUImage#processing-a-still-image


我已经使用了GPUImage和选择性模糊来实现模糊效果,但我的问题是如何获得设置模糊效果的预览效果,就像在Instagram中更改模糊半径或位置时所显示的白色图层一样。这种预览效果能够指示模糊的位置。我的问题是如何实现这个预览效果。 - Eyal
你尝试过链接GPUImage滤镜吗?比如将模糊与白色晕映或颜色叠加链接起来? - brynbodayle
请尝试使用GPUImage来使用以下两种方法: stillImageSource removeAllTargets blurFilter removeAllTargets其中, GPUImageOutput<GPUImageInput> *blurFilter; GPUImagePicture *stillImageSource; - kb920
我认为使用另一个滤镜来做这个可能不明智,每次用户移动手指改变模糊效果都会影响性能。实际上,当用户移动手指时,我并不需要显示模糊效果,我只需要显示预览层(只是一个圆形 - 没有模糊效果)。所以这不是一个关于滤镜的问题,而更多是关于核心图形的问题... - Eyal

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