一行程式碼實現 UIView 鏤空效果

雷曼同學發表於2018-11-15

一行程式碼實現 UIView 鏤空效果

這是一種實現 UIView 鏤空效果的方案,可以快速實現任意形狀的鏤空、文字的鏤空、帶鏤空的毛玻璃效果等。本質上是 UIViewmaskView 效果的「取反」。

前言

首先來複習一下遮罩效果的實現。如果我們有一張圖片,又恰好有一個圓,當我們把圓設定為圖片的遮罩時,會得到這樣的結果。

一行程式碼實現 UIView 鏤空效果

程式碼實現看上去像是這樣:

view.maskView = maskView;
複製程式碼

那麼問題來了,如果我們希望得到下面的結果,該怎麼做呢?這看起來像是圖層的相減,即原來的圖層減去遮罩的部分。

一行程式碼實現 UIView 鏤空效果

可惜蘋果爸爸不夠貼心,沒有提供方便的介面呼叫。讓我們來看看可以怎麼實現。

一、思路

我們的最終目標是,封裝出一個介面,呼叫方式類似於 maskView 屬性,可以很方便地對一個 UIView 做鏤空效果。

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

目前看來,可以從兩個方向入手:

  1. 修改遮罩的繪製過程
  2. 修改 maskView 本身

方式一是指,在設定這個屬性的時候,對 originView 的檢視進行重新繪製,然後在繪製的時候,減掉 maskView 的區域。

方式二是指,當拿到 maskView 的時候,先對 maskView 本身先進行處理,將遮罩範圍取反。然後再做遮罩效果,由於遮罩的區域已經相反,於是得到的結果也是相反的,就達到鏤空的目的。

看上去方式二比較靠譜,而且最後是呼叫 UIViewsetMaskView: 來實現,還可以保留原來遮罩的一些特性。比如當修改 maskViewframe 的時候, originView 的遮罩位置也會相應改變。

二、實現

生成相反的遮罩圖可以分為三步。假設一開始拿到的 maskView 是下面這樣,讓我們來看下,轉換過程中遮罩圖每一步的變化。

一行程式碼實現 UIView 鏤空效果

注: 為了更直觀的效果,圖片中透明的部分用灰白相間格子來表示(以下相同)。

1、將 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 的大小。

一行程式碼實現 UIView 鏤空效果

2、將 UIImage 轉換為只有 alpha 通道的 CGContextRef

CGImageRef originalMaskImage = [image CGImage];
float width = CGImageGetWidth(originalMaskImage);
float height = CGImageGetHeight(originalMaskImage);
    
int strideLength = ceil(width);
unsigned char * alphaData = calloc(strideLength * height, sizeof(unsigned char));
CGContextRef alphaOnlyContext = CGBitmapContextCreate(alphaData,
                                                      width,
                                                      height,
                                                      8,
                                                      strideLength,
                                                      NULL,
                                                      kCGImageAlphaOnly);
    
CGContextDrawImage(alphaOnlyContext, CGRectMake(0, 0, width, height), originalMaskImage);
複製程式碼

這時候的 alphaOnlyContext 對應的影像是下面這樣,只保留了 alpha 通道。

一行程式碼實現 UIView 鏤空效果

3、將 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 影像是:

一行程式碼實現 UIView 鏤空效果

於是,我們就可以用 result 愉快地進行 mask 了。

三、使用

我們可以將上述的步驟,封裝為一個方法,用 category 來實現。

@interface UIView (MFSubtractMask)

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

- (UIView *)subtractMaskView;

@end
複製程式碼

這樣呼叫起來就十分方便了,一行程式碼搞定:

view.subtractMaskView = maskView;
複製程式碼

四、侷限性

1. subtractMaskView 不會自動重新整理

我們知道,當 UIViewmaskView 的內容動態修改時,會實時反映到 UIView 中。但在本專案中, subtractMaskView 屬性會生成一張全新的圖片來作為遮罩圖,因為不會根據 subtractMaskView 的內容實時來重新整理檢視。如果需要更新,必須手動呼叫 setSubtractMaskView: 方法來重新生成遮罩圖。

2. setSubtractMaskView: 不宜被頻繁呼叫

setSubtractMaskView: 本質上是生成一個新的遮罩圖的過程,該過程涉及圖片畫素的遍歷轉換,較為耗時,不宜頻繁呼叫。

綜上所述,這種方案適合只生成一次遮罩圖的場景。

五、原始碼

請到 GitHub 上檢視完整程式碼。

參考

獲取更佳的閱讀體驗,請訪問原文地址 【Lyman's Blog】一行程式碼實現 UIView 鏤空效果

相關文章