UIControl
UIControl繼承自UIView。
UIControl 依賴于Target-Action設計模式。即當發生一個事件時,UIControl會調用sendAction:to:forEvent:方法來將行為消息發送到UIApplication對象,再由UIApplication對象調用其sendAction:to:fromSender:forEvent:方法來將消息分發到指定的target上。如果沒有指定target,則會將事件分發到響應鏈上第一個想處理該消息的對象上。
UIControl有不同的狀態
typedef NS_OPTIONS(NSUInteger, UIControlState) {
UIControlStateNormal = 0,
UIControlStateHighlighted = 1 << 0, // used when UIControl isHighlighted is set
UIControlStateDisabled = 1 << 1,
UIControlStateSelected = 1 << 2, // flag usable by app (see below)
UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 3, // Applicable only when the screen supports focus
UIControlStateApplication = 0x00FF0000, // additional flags available for application use
UIControlStateReserved = 0xFF000000 // flags reserved for internal framework use
};
通過繼承UIControl類,就可以使用OC內建的 target-action 機制以及簡化版的 event-handling。主要有以下兩類方法來實現UIControl。
重寫
sendAction:to:forEvent:
方法。這樣就可以觀察或者改寫OC的分發機制,從而達到監聽某個特定的對象(object)對于特定的事件(event)做了什么特定的處理(selector)。進一步的可以攔截到這些對象的事件,把它們發送到其他對象,或者讓本對象執行其他的方法。重寫
beginTrackingWithTouch:withEvent:,
continueTrackingWithTouch:withEvent:,
endTrackingWithTouch:withEvent:,
cancelTrackingWithEvent:
等方法。這樣就可以追蹤并獲取到control對象的狀態。進一步的,可以依據這些狀態去更新頁面上控件的狀態;或者調用某些方法,執行其他命令。
此處需要注意,蘋果文檔上有一句:Always use these methods to track touch events instead of the methods defined by the UIResponder class.
不知為何,蘋果要這樣寫。
UIResponder
UIResponder對象及其子類的對象都叫做響應者。繼承關系如下圖:
也就是說UIApplication,UIViewCOntroller,UIView都是響應者,都可以接收并處理事件。
響應者是響應事件的。在iOS中,事件分為三種。即觸摸事件,加速計事件,遠程控制事件。
一般開發中觸摸事件使用最頻繁,而且其他兩種事件處理方式與觸摸事件大同小異,所以只介紹觸摸事件。
觸摸事件包括:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
可以看出,觸摸事件包括用戶交互的整個過程。包括觸摸開始,用戶滑動,觸摸結束,以及觸摸因為其他事件(比如來電話)被取消。
以上方法中都包含兩個參數:touches和event。
touches是一個包含UITouch對象的集合。
UITouch對象記錄著某個事件中手指的相關信息,比如位置,大小,運動狀況,手指在屏幕上的壓力(限于有3D Touch的手機)等。
主要屬性有:
- window 觸摸所在的窗口
- view 觸摸所在的視圖
- tapCount 點擊屏幕的次數
- majorRadius 觸摸范圍半徑。錘子科技的Big-Bang就是根據觸摸半徑的大小來判斷是否“炸開”文字的。
- gestureRecognizers 手勢數組。如果觸摸事件是發生在view對象上的,給這個view對象添加的手勢UIGestureRecognizer都會在這個數組中。如果view對象沒有添加過手勢,這個數組中也有一個系統手勢:_UISystemGestureGateGestureRecognizer。
UIGestureRecognizer
手勢識別。蘋果文檔中有這樣一段描述值得注意:
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled. The usual sequence of actions in gesture recognition follows a path determined by default values of the
cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded
properties:
即,當觸摸事件發生時,如果,手勢對象會先于view對象獲取到觸摸事件。如果這個手勢對象可以處理該事件,那么view對象就不會接收到觸摸事件。如果還想讓view也接收到事件,就要把手勢的cancelsTouchesInView屬性設置為NO。
具體參見以下代碼:
//給一個view對象添加UIButton子控件。
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(10, 20, 200, 20)];
[btn setTitle:@"button" forState:UIControlStateNormal];
//btn對象添加 target-action
[btn addTarget:self action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:btn];
UITapGestureRecognizer *btnTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(buttonTapAction)];
//設置cancelsTouchesInView屬性。
btnTap.cancelsTouchesInView = NO;
[btn addGestureRecognizer:btnTap];
以上代碼,當cancelsTouchesInView
為YES時。只響應buttonTapAction
方法;當為NO時,事件可以繼續傳遞,buttonTapAction
和buttonAction
方法均響應。
UIGestureRecognizer 響應始終在主線程。測試代碼中曾把添加手勢的代碼放在子線程中,結果發現手勢的響應仍然是在主線程。我猜測是這樣的,手勢的添加不在乎在哪個線程,只要把手勢添加到view上即可。觸摸事件的發生以及傳遞是在主線程的。所以,我們的響應方法最終在主線程被執行。
但是,文檔中有一句我不是很明白A gesture recognizer doesn’t participate in the view’s responder chain.
查了一些資料,還是沒有頭緒,這個問題先記著,后續處理。//TODO:find the answer.
事件傳遞
問題來了,如果UIResponder,UIGestureRecognizer,UIControl各自的對象同時出現一個或者多個;又或者他們三個中不同的對象同時出現,那響應順序是什么樣子的呢?
- 上面分析過,UIGestureRecognizer和UIControl同時存在時。會優先處理UIGestureRecognizer,如果事件能夠響應,則不再處理UIControl。
- 如果view對象在添加了UIGestureRecognizer手勢的同時,也實現了UIResponder的方法,比如
touchBegin
。那響應順序如何?以下是我的測試:
如下圖的結構:
UITestViewB和UITestViewA都是UIView的子類。并且都添加了單擊手勢
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)];
同時,實現了- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
方法
我測試的結果是先響應UIResponder方法,再響應UIGestureRecognizer。
另外,如果想要UIResponder繼續傳遞,那就直接調用super方法,觸摸事件就可以接著傳遞給父控件;
如果想要UIGestureRecognizer繼續傳遞,那就重寫可以同時響應的代理方法--gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
。雖然這個方法的說法是可以讓一個對象同時響應多個手勢,但經過測試發現,當這個方法返回YES時,父子控件可以同時響應一個觸摸事件。如果父子控件的這個代理方法都返回NO(或者不寫,默認是NO),那么只有子控件響應觸摸事件。
這里我還有一個問題,沒有解決。就是UIControl了類的點擊事件如何繼續傳遞。情景是這樣的。一個view中添加一個button子控件。butoon通過addTarget添加target-action。如何在點擊button后,讓該點擊事件繼續傳遞到父控件view上。view實現了toucheBegin
并且也添加了tap手勢。
我能想到的方法是,給button再次添加target-action。即在點擊button是發出給兩個target發送message,其中一個message是view。即把觸摸事件發送給view。但是這樣只能夠實現相同的效果,并不是把同一個觸摸事件傳遞給view。雖然應該不會有這樣的需求,但我只是好奇這能夠實現嗎?