一行代码实现 UIView 镂空效果

640?wx_fmt=gif

640?wx_fmt=png

程序君个人微信 和我聊聊编程和创业的事 加好友


作者丨雷曼同学

http://www.lymanli.com/2018/11/10/subtract-mask/


这是一种实现 UIView 镂空效果的方案,可以快速实现任意形状的镂空、文字的镂空、带镂空的毛玻璃效果等。本质上是 UIView 的 maskView 效果的「取反」。


640?wx_fmt=gif

前言


首先来复习一下遮罩效果的实现。如果我们有一张图片,又恰好有一个圆,当我们把圆设置为图片的遮罩时,会得到这样的结果。


640?wx_fmt=jpeg


代码实现看上去像是这样:


view.maskView = maskView;


那么问题来了,如果我们希望得到下面的结果,该怎么做呢?这看起来像是图层的相减,即原来的图层减去遮罩的部分。


640?wx_fmt=jpeg


可惜苹果爸爸不够贴心,没有提供方便的接口调用。让我们来看看可以怎么实现。


640?wx_fmt=gif

一、思路


我们的最终目标是,封装出一个接口,调用方式类似于 maskView 属性,可以很方便地对一个 UIView 做镂空效果。


**注:**以下用 originView 指代需要上效果的 view,用 maskView 指代充当遮罩的 view。


目前看来,可以从两个方向入手:


  1. 修改遮罩的绘制过程

  2. 修改 maskView 本身


方式一是指,在设置这个属性的时候,对 originView 的视图进行重新绘制,然后在绘制的时候,减掉 maskView 的区域。


方式二是指,当拿到 maskView 的时候,先对 maskView 本身先进行处理,将遮罩范围取反。然后再做遮罩效果,由于遮罩的区域已经相反,于是得到的结果也是相反的,就达到镂空的目的。


看上去方式二比较靠谱,而且最后是调用 UIView 的 setMaskView: 来实现,还可以保留原来遮罩的一些特性。比如当修改 maskView 的 frame 的时候, originView 的遮罩位置也会相应改变。


640?wx_fmt=gif

二、实现


生成相反的遮罩图可以分为三步。假设一开始拿到的 maskView 是下面这样,让我们来看下,转换过程中遮罩图每一步的变化。


640?wx_fmt=jpeg


**注:**为了更直观的效果,图片中透明的部分用灰白相间格子来表示(以下相同)。


640?wx_fmt=gif1、将 maskView 转化为 UIImage


UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale);
CGContextTranslateCTM(UIGraphicsGetCurrentContext(),
                      view.frame.origin.x,
                      view.frame.origin.y);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();


这一步拿到了 maskView 对应的 image 图像。此时遮罩图的大小会被同步为 originView 的大小。


640?wx_fmt=jpeg


640?wx_fmt=gif2、将 UIImage 转换为只有 alpha 通道的 CGContextRef


CGImageRef originalMaskImage = [image CGImage];
float width = CGImageGetWidth(originalMaskImage);
float height = CGImageGetHeight(originalMaskImage);

int strideLength = ROUND_UP(width * 14);
unsigned char * alphaData = calloc(strideLength * height, sizeof(unsigned char));
CGContextRef alphaOnlyContext = CGBitmapContextCreate(alphaData,
                                                      width,
                                                      height,
                                                      8,
                                                      strideLength,
                                                      NULL,
                                                      kCGImageAlphaOnly);

CGContextDrawImage(alphaOnlyContext, CGRectMake(00, width, height), originalMaskImage);


这时候的 alphaOnlyContext 对应的图像是下面这样,只保留了 alpha 通道。


640?wx_fmt=jpeg


640?wx_fmt=gif3、将 CGContextRef 中的 alpha 值进行遍历转换


for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        unsigned char val = alphaData[y*strideLength + x];
        val = 255 - val;
        alphaData[y*strideLength + x] = val;
    }
}

CGImageRef alphaMaskImage = CGBitmapContextCreateImage(alphaOnlyContext);
UIImage *result = [UIImage imageWithCGImage:alphaMaskImage];


转换后,获得的 result 图像是:


640?wx_fmt=other


于是,我们就可以用 result 愉快地进行 mask 了。


640?wx_fmt=gif

三、使用


我们可以将上述的步骤,封装为一个方法,用 category 来实现。


@interface UIView (MFSubtractMask)

- (void)setSubtractMaskView:(UIView *)view;

- (UIView *)subtractMaskView;

@end


这样调用起来就十分方便了,一行代码搞定:


view.subtractMaskView = maskView;


640?wx_fmt=gif

四、局限性


640?wx_fmt=gif1. subtractMaskView 不会自动刷新


我们知道,当 UIView 的 maskView 的内容动态修改时,会实时反映到 UIView 中。但在本项目中, subtractMaskView 属性会生成一张全新的图片来作为遮罩图,因为不会根据 subtractMaskView 的内容实时来刷新视图。如果需要更新,必须手动调用 setSubtractMaskView: 方法来重新生成遮罩图。


640?wx_fmt=gif2. setSubtractMaskView: 不宜被频繁调用


setSubtractMaskView: 本质上是生成一个新的遮罩图的过程,该过程涉及图片像素的遍历转换,较为耗时,不宜频繁调用。


综上所述,这种方案适合只生成一次遮罩图的场景。


640?wx_fmt=gif

五、源码


GitHub 地址


https://github.com/lmf12/MFSubtractMask


 推荐↓↓↓ 

640?wx_fmt=png

?16个技术公众号】都在这里!

涵盖:程序员大咖、源码共读、程序员共读、数据结构与算法、黑客技术和网络安全、大数据科技、编程前端、Java、Python、Web编程开发、Android、iOS开发、Linux、数据库研发、幽默程序员等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值