iOS實現UIImageView透明區域點擊事件穿透

原文轉自:http://wonderffee.github.io/blog/2013/07/10/pass-touch-event-through-uiimageviews-transparent-area-in-ios/

問題

最近要在iPad上實現一個很獨特的功能,簡單描述一下就是要顯示一個帶有半透明背景的彈出界面,在其上加一個不規則形狀的圖片,手指點擊這個彈出界面的半透明區域就退出這個彈出界面。

問題是UED/美工不會提供純粹的不規則形狀切圖,實際只能給出的是以不規則形狀加透明區域的矩形切圖,這就帶來另外一個要求:點擊矩形切圖的透明區域也要退出彈出界面。這就有點難辦了,透明區域也是不規則形狀的,該怎麼判斷出手指點擊的點就是透明區域呢?

思路

一般在iOS的控件中,要不就是完全允許用戶點擊,要不就是禁止用戶交互,這是可以通過設置控件的userInteractionEnabled屬性來修改。如果添加的圖片不是不規則形狀的,而是矩形,這問題就簡單多了,只需要將矩形圖片對應的UIImageView的userInteractionEnabled設爲YES,對半透明背景View(或者直接設置爲一個按鈕)設置點擊事件處理,就可以點擊實現半透明背景退出彈出界面。

現在的情況是這個矩形圖片一分爲二,一部分爲實體的不規則形狀圖片,一部分爲不規則形狀的透明區域。很顯然,問題的解決思路是:讓手指能“穿透”這個不規則透明區域去點擊背後的半透明背景,而不透明部分就不“穿透”。

前面說的userInteractionEnabled屬性只是簡單地一刀切設置控件是否允許用戶操作(即可以響應手指觸摸事件),更加靈活的設置方法是使用UIView的hitTest:withEvent:與pointInside:withEvent:。簡單介紹下,iOS中的pointInside:withEvent:方法是用來判斷當前的點擊或者觸摸事件的點是否在當前的view中,它被hitTest:withEvent:調用,通過對每個子視圖調用pointInside:withEvent:決定最終哪個視圖來響應此事件。如果一個子視圖的pointInside:withEvent:返回NO,說明這個子視圖不會響應點擊事件,然後就去尋找更深層的子視圖來找到最終響應觸摸事件;返回YES就說明子視圖能響應點擊事件(但不一定是子視圖本身響應,若子視圖還有子視圖的話,還會繼續循環去找最終響應事件的子子視圖)。

於是,本文的問題就可以這樣轉化:創建一個UIImageView的子類,重寫pointInside:withEvent:方法,讓矩形圖片的透明區域的pointInside:withEvent:返回NO,而非透明區域的pointInside:withEvent:返回YES,如果能達到這個要求,透明區域點擊事件穿透就能夠實現。

現在的關鍵問題是怎麼識別出這個透明區域。 
iOS中通常用的圖片是PNG圖片,這種圖片有alpha通道,如果能獲取PNG圖片每個像素的alpha值,就不難判斷出手指點擊的圖片區域是不是透明的。

關鍵代碼如下:

Here’s Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    //Using code from http://stackoverflow.com/questions/1042830/retrieving-a-pixel-alpha-value-for-a-uiimage

    unsigned char pixel[1] = {0};
    CGContextRef context = CGBitmapContextCreate(pixel,
                                                 1, 1, 8, 1, NULL,
                                                 kCGImageAlphaOnly);
    UIGraphicsPushContext(context);
    [self.image drawAtPoint:CGPointMake(-point.x, -point.y)];
    UIGraphicsPopContext();
    CGContextRelease(context);
    CGFloat alpha = pixel[0]/255.0f;
    BOOL transparent = alpha < 0.01f;

    return !transparent;
}

解釋: 
這段代碼是通過CGBitmapContextCreate方法創建只包含alpha通道的圖形上下文(真不知道context怎麼翻譯爲最好),這個圖形上下文的大小爲1x1,也就是實際上只放得下一個像素,將矩形圖片手指觸摸點point繪製到這個圖形上下文中,那麼pixel數組中唯一元素的值就是手指觸摸點那一個像素的alpha值,做歸一化爲與0.01比較,如果小於0.01就表明手指觸摸點是透明的,這時候返回NO就能夠實現穿透效果,相反大於0.01就不會穿透。

注意到代碼中用到的座標爲(-point.x, -point.y),爲什麼會是負數呢?這是因爲如果context的區域大小與image一致的話,[image drawAtPoint:]就會將image全部繪製在context中,而實際上context只放得下一個像素,爲了保證point點能剛好繪製在這個context上,就必須設置繪製的起始座標爲(-point.x, -point.y)。

代碼中的UIGraphicsPushContext容易誤導人,看名字以爲是將參數中指定的context push入棧,但是參數中的context明明就是剛創建的啊?其實它是將舊的context(默認的context)入棧,再切換到新的context(也就是參數中指定的)繪製,執行UIGraphicsPopContext後就會切換回舊的context,而在新的context上繪製的內容完全不影響舊context(默認context)。這與CGContextSaveGState和CGContextRestoreGState是有本質區別的。

附CGBitmapContextCreate函數參數詳解: 
原型:

1
2
3
4
5
6
7
8
9
CGContextRef CGBitmapContextCreate (
   void *data,
   size_t width,
   size_t height,
   size_t bitsPerComponent,
   size_t bytesPerRow,
   CGColorSpaceRef colorspace,
   CGBitmapInfo bitmapInfo
);

參數: 
data 指向要渲染的繪製內存的地址。這個內存塊的大小至少是(bytesPerRow*height)個字節 
width bitmap的寬度,單位爲像素 
height bitmap的高度,單位爲像素 
bitsPerComponent 內存中像素的每個組件的位數.例如,對於32位像素格式和RGB 顏色空間,你應該將這個值設爲8. 
bytesPerRow bitmap的每一行在內存所佔的比特數 
colorspace bitmap上下文使用的顏色空間。 
bitmapInfo 指定bitmap是否包含alpha通道,像素中alpha通道的相對位置,像素組件是整形還是浮點型等信息的字符串。

描述: 
當你調用這個函數的時候,Quartz創建一個位圖繪製環境,也就是位圖上下文。當你向上下文中繪製信息時,Quartz把你要繪製的信息作爲位圖數據繪製到指定的內存塊。一個新的位圖上下文的像素格式由三個參數決定:每個組件的位數,顏色空間,alpha選項。alpha值決定了繪製像素的透明性。

參考資料:

 Jul 10th, 2013

原創文章,版權聲明:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0


發佈了105 篇原創文章 · 獲贊 18 · 訪問量 65萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章