概述
在正常的使用場景中,我們處理了比較多的矩形區域內觸摸事件,比如UIButton、UIControl。一般來說,這些控件的圖形以及觸摸區域都是矩形或者圓角矩形的。但是在一些特殊應用場景中我們有時不得不面對這樣一種比較嚴苛的需求,比如要求程序只對某個圓形、五角形等非常規區域的點擊事件進行處理,這就需要花點功夫了。本文以圓形為例子來介紹此類場景的處理方法。
先看下面一張圖(附圖1),我們的目標是實現如下自定義tabbar。中間帶突起圓形的自定義tabbar曾一度流行,今天我們來粗糙地實現一下。
在附圖一中,紅色代表tabbar,上面有三個藍色按鈕。在三個按鈕中我們重點解決按鈕A,因為它有一半的區域突在tabbar的有效區域外。
對於按鈕A,我們有以下兩個問題需要解決:
1、如何准確過濾掉A外接矩形里非藍色區域的點擊事件?
2、如何讓A的上半部分也能響應觸摸事件?
其實兩個問題的解決方法是基本一致的。在iOS中所有控件都是以矩形的方式存在的,在圖2中盡管藍色部分看起來是圓形,但當點擊外接矩形內的非圓形區域時也會默認觸發點擊事件。因此,我們需要用一些手段把觸摸事件“攔截”下來。想要“攔截”事件,就必須了解iOS的事件分發機制,也就是當你點擊設備屏幕后,iOS是如何決定由那個view去最終響應你的觸摸!下面插播一小段關於iOS事件分發的介紹:
==================================
當你手指觸摸屏幕后會發生以下事情:觸摸事件被封裝成一個UIEvent事件,去當前iOS操作系統的active app隊列中取當前活躍的APP,把event傳給它--->event傳給UIApplication--->傳給UIWindow的root view controller(rootVC)--->調用rootVC.view的所有subviews的hitTest:event:方法。哪個view的hitTest:event方法返回非nil值,則觸摸事件就交給該view處理。關於事件分發的詳細機制及舉例可以參考技術哥大神的文章
==================================
分析
讓我們重新回到探討的問題上。通過以上簡介我們可以知道,想“攔截”觸摸事件,則應該在tabbar的hitTest:event方法中做處理(坐標判斷等)。以下是具體的demo源碼:
#import <UIKit/UIKit.h> @interface panelView : UIView @end
1 #import "panelView.h" 2 3 @implementation panelView 4 5 - (id)initWithFrame:(CGRect)frame 6 { 7 self = [super initWithFrame:frame]; 8 if (self) { 9 [self initSubviews]; 10 } 11 return self; 12 } 13 14 - (void)initSubviews 15 { 16 UIButton *roundBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 17 roundBtn.frame = CGRectMake(self.frame.size.width / 2 - 30, -30, 60, 60); 18 roundBtn.backgroundColor = [UIColor blueColor]; 19 roundBtn.layer.cornerRadius = 30; 20 roundBtn.tag = 10086; 21 [roundBtn addTarget:self action:@selector(onBtnPressed:) 22 forControlEvents:UIControlEventTouchUpInside]; 23 [self addSubview:roundBtn]; 24 25 UIButton *leftBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 26 leftBtn.frame = CGRectMake(0, 15, 30, 30); 27 leftBtn.backgroundColor = [UIColor blueColor]; 28 leftBtn.tag = 10087; 29 [leftBtn addTarget:self action:@selector(onBtnPressed:) 30 forControlEvents:UIControlEventTouchUpInside]; 31 [self addSubview:leftBtn]; 32 33 UIButton *rightBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 34 rightBtn.frame = CGRectMake(self.frame.size.width - 30, 15, 30, 30); 35 rightBtn.backgroundColor = [UIColor blueColor]; 36 rightBtn.tag = 10088; 37 [rightBtn addTarget:self action:@selector(onBtnPressed:) 38 forControlEvents:UIControlEventTouchUpInside]; 39 [self addSubview:rightBtn]; 40 } 41 42 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 43 { 44 UIView *hitView = nil; 45 //NSLog(@"point:%@", NSStringFromCGPoint(point)); 46 UIButton *roundBtn = (UIButton *)[self viewWithTag:10086]; 47 UIButton *leftBtn = (UIButton *)[self viewWithTag:10087]; 48 UIButton *rightBtn = (UIButton *)[self viewWithTag:10088]; 49 BOOL pointInRound = [self touchPointInsideCircle:roundBtn.center radius:30 targetPoint:point]; 50 if (pointInRound) { 51 hitView = roundBtn; 52 } else if(CGRectContainsPoint(leftBtn.frame, point)) { 53 hitView = leftBtn; 54 } else if(CGRectContainsPoint(rightBtn.frame, point)) { 55 hitView = rightBtn; 56 } else { 57 hitView = self; 58 } 59 return hitView; 60 } 61 62 - (BOOL)touchPointInsideCircle:(CGPoint)center radius:(CGFloat)radius targetPoint:(CGPoint)point 63 { 64 CGFloat dist = sqrtf((point.x - center.x) * (point.x - center.x) + 65 (point.y - center.y) * (point.y - center.y)); 66 return (dist <= radius); 67 } 68 69 70 - (void)onBtnPressed:(id)sender 71 { 72 UIButton *btn = (UIButton *)sender; 73 NSLog(@"btn tag:%d", btn.tag); 74 } 75 76 @end
在hitTest方法中最重要的是判斷按鈕A所在的區域,其實僅僅用到兩點的距離公式來圈出藍色部分所在的圓形,判斷方法如下:
- (BOOL)touchPointInsideCircle:(CGPoint)center radius:(CGFloat)radius targetPoint:(CGPoint)point
{
CGFloat dist = sqrtf((point.x - center.x) * (point.x - center.x) +
(point.y - center.y) * (point.y - center.y));
return (dist <= radius);
}
而判斷點是否在按鈕B/C內就更簡單了,系統提供了封裝好的api:
bool CGRectContainsPoint(CGRect rect, CGPoint point)
最終,關於事件“攔截”的判斷如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = nil;
//NSLog(@"point:%@", NSStringFromCGPoint(point));
UIButton *roundBtn = (UIButton *)[self viewWithTag:10086];
UIButton *leftBtn = (UIButton *)[self viewWithTag:10087];
UIButton *rightBtn = (UIButton *)[self viewWithTag:10088];
BOOL pointInRound = [self touchPointInsideCircle:roundBtn.center radius:30 targetPoint:point];
if (pointInRound) {
hitView = roundBtn;
} else if(CGRectContainsPoint(leftBtn.frame, point)) {
hitView = leftBtn;
} else if(CGRectContainsPoint(rightBtn.frame, point)) {
hitView = rightBtn;
} else {
hitView = self;
}
return hitView;
}
此外,在hitTest中還可以玩其他花樣,比如將本該由按鈕A響應的時間強制性轉發給其他按鈕,這只需在hitTest的返回值中修改一下即可!
本文所用demo完整源碼點這里test.zip下載。
圖1 圖2
=======================================================
原創文章,轉載請注明 編程小翁@博客園,郵件zilin_weng@163.com,歡迎各位與我在C/C++/Objective-C/機器視覺等領域展開交流!
歡迎跳轉我的GitHub主頁,關注我的開源代碼。也歡迎你Star/Fork/Watch我的項目。
=======================================================
