UIButton继承链
首先看一下继承关系:
@interface UIButton : UIControl <NSCoding>
@interface UIControl : UIView
@interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, CALayerDelegate>
@interface UIResponder : NSObject <UIResponderStandardEditActions>
上面是UIButton的继承链。在<objc/runtime.h>中有objc_class的定义:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
结构体中定义了objc_protocol_list,用于存储协议。我们在对一个类进行分析时,除了要看类自身的方法,以及通过继承拥有的方法,还需要去看协议中定义了那些方法。
UIButton
首先,我们来看一下UIButton自身包含哪些属性和方法。
@property(nullable, nonatomic,readonly,strong) UILabel *titleLabel;
@property(nullable, nonatomic,readonly,strong) UIImageView *imageView;
UIButton是集成了UILable和UIImageView,用于信息展示。
@property(nonatomic,readonly) UIButtonType buttonType;
通过buttonType属性的设置,可以实现不同样式的button。
- (void)setTitle:(nullable NSString *)title forState:(UIControlState)state;
- (void)setTitleColor:(nullable UIColor *)color forState:(UIControlState)state;
- (void)setTitleShadowColor:(nullable UIColor *)color forState:(UIControlState)state;
- (void)setImage:(nullable UIImage *)image forState:(UIControlState)state;
- (void)setBackgroundImage:(nullable UIImage *)image forState:(UIControlState)state;
- (void)setAttributedTitle:(nullable NSAttributedString *)title forState:(UIControlState)state;
通过上述方法,设置UIButton的属性值。
@property(nonatomic) UIEdgeInsets contentEdgeInsets
@property(nonatomic) UIEdgeInsets titleEdgeInsets;
@property(nonatomic) UIEdgeInsets imageEdgeInsets;
调整内容视图的位置。
+ (instancetype)buttonWithType:(UIButtonType)buttonType;
初始化UIButton的方法。 以上是UIButton的一些方法和属性。接下来我们看一下UIControl。
UIControl
首先,我们来看一下都有哪些类继承自UIControl。
- UIButton
- UIDatePicker
- UIPageControl
- UISegmentedControl
- UISlider
- UIStepper
- UISwitch
- UITextField
- UITextView
差不多所有接收用户输入的控件都会继承UIControl。接下来,我们来看一下UIControl的API。
@property(nonatomic,getter=isEnabled) BOOL enabled;
默认值是YES,值为NO时,忽略touch事件。
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;
对手指触摸做处理,产生相应的事件。
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
- (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(UIContorlEvents)controlEvents;
添加事件监听、移除事件监听。
- (nullable NSArray<NSString *> *)actionsForTarget:(nullable id)target forControlEvent:(UIControlEvents)controlEvent;
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents;
可以重写上述方法,改变方法实现。
UIView
@property(nonatomic,readonly,strong) CALayer *layer;
为什么要把layer属性放在第一位?查看过CALayer文档,我们发现视图内容的显示是layer实现的。layer有contents属性,类型是id。我们可以通过赋值contents来显示内容。将UIView的API和CALayer的API作对比,可以发现两者是有很多共通之处的,比如:
CALayer
- (void)removeFromSuperlayer;
- (void)insertSublayer:(CALayer *)layer atIndex:(unsigned)idx;
- (void)replaceSublayer:(CALayer *)layer with:(CALayer *)layer2;
- (void)addSublayer:(CALayer *)layer;
- (void)insertSublayer:(CALayer *)layer below:(nullable CALayer *)sibling;
- (void)insertSublayer:(CALayer *)layer above:(nullable CALayer *)sibling;
- (nullable CALayer *)hitTest:(CGPoint)p;
- (BOOL)containsPoint:(CGPoint)p;
- (void)drawInContext:(CGContextRef)ctx;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)r;
UIView
- (void)removeFromSuperview;
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index;
- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2;
- (void)addSubview:(UIView *)view;
- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview;
- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview;
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (CGPoint)convertPoint:(CGPoint)p fromLayer:(nullable CALayer *)l;
- (CGPoint)convertPoint:(CGPoint)p toLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r fromLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r toLayer:(nullable CALayer *)l;
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;
- (void)drawRect:(CGRect)rect;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
方法完全类似。
CALayer
@property CGRect frame;
@property CGRect bounds;
@property CGPoint position;
@property CATransform3D transform;
... ...
UIView
@property(nonatomic) CGRect frame;
@property(nonatomic) CGRect bounds;
@property(nonatomic) CGPoint center;
@property(nonatomic) CGAffineTransform transform;
... ...
经过上述对比,我们可以认为UIView是对CALayer,以及CAAnimation的封装。CALLayer是视图展示层,UIView是视图展示层控制层。
感兴趣的朋友可以去做更深层次的对比。
对于UIView遵循的一系列协议,本篇就不一一详解了,感兴趣的朋友可以去翻阅一下。
接下来我们来看一下UIResponder。
UIResponder
我们首先来看几个属性和方法
- (nullable UIResponder*)nextResponder;
// touch
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// press
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event;
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event;
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event;
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event;
// motion
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event;
// remoteControl
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event;
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender;
- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender
上述代码中可以看出来,UIResponder主要是针对于事件的响应。
@interface UIResponder (UIResponderInputViewAdditions)
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputView;
@property (nullable, nonatomic, readonly, strong) __kindof UIView *inputAccessoryView;
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputViewController;
@property (nullable, nonatomic, readonly, strong) UIInputViewController *inputAccessoryViewController;
UIResponder还有其他的类别及协议。我们就先说到这里。
接下来我们尝试去看一下UIButton的事件响应过程。
UIButton响应链
试想一下,如果我们点击了屏幕上的某一个区域,并希望程序能响应一些操作,整个流程应该是怎么样的呢?
事件分发
首先,我们要知道我们点击的是哪个控件。如果我们站在应用程序的角度,我们不可能根据手指点击的位置直接获取到包含当前点的最小视图。我们需要从最大的视图(UIWindow)去层层向内递归,直至找到最小视图。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.alpha < 0.01 || self.userInteractionEnabled || self.hidden) {
return nil;
}
if (![self pointInside:point withEvent:event]) {
return nil;
}
__block UIView *hitView = self;
[self.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
hitView = [obj hitTest:point withEvent:event];
if (hitView) {
*stop = YES;
}
}];
return hitView ? : self;
}
系统在寻找最小视图时使用的方法:
// recursively calls -pointInside:withEvent: point is in the receivers coordinate system
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
// default returns YES if point is in bounds
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
接下来我们实际测试一下
#import "UIView+ResponderChain.h"
#import <objc/runtime.h>
@implementation UIView (ResponderChain)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class currentClass = [self class];
SEL originSel = @selector(hitTest:withEvent:);
SEL newSel = @selector(customHitTest:withEvent:);
Method originMethod = class_getInstanceMethod(currentClass, originSel);
Method newMethod = class_getInstanceMethod(currentClass, newSel);
BOOL addSuccess = class_addMethod(currentClass, originSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
if (addSuccess) {
class_replaceMethod(currentClass, newSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
} else {
method_exchangeImplementations(originMethod, newMethod);
}
});
}
- (UIView *)customHitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ %s", [self class], __FUNCTION__);
return [self customHitTest:point withEvent:event];
}
@end
我们在视图控制器中添加view如下:
- (void)viewDidLoad {
[super viewDidLoad];
AView *redView = [[AView alloc] initWithFrame:CGRectMake(30, 30, 200, 200)];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
BView *blueView = [[BView alloc] initWithFrame:CGRectMake(20, 20, 160, 160)];
blueView.backgroundColor = [UIColor blueColor];
[redView addSubview:blueView];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.backgroundColor = [UIColor lightGrayColor];
btn.frame = CGRectMake(20, 20, 120, 120);
[btn addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
[blueView addSubview:btn];
}
当我们点击button时, 我们来看一下输出结果:
现在我们已经找到了最小视图了,下面就是事件响应了。
事件响应
事件的响应依赖的是UIResponder,前文我们已经看过UIResponder的API了,对于事件的响应(以点击事件为例),UIResponder提供了touchesBegan、touchesMoved、touchesEnded、touchesCancelled。
我们在UIView的类别中通过方法替换替换touchBegan:
- (void)customTouchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
[super touchesBegan:touches withEvent:event];
}
写到这里,整个事件的处理流程以及结束。事件的分发依赖的是图层,通过point定位到最小视图,然后通过UIResponder反向查询响应链。如果事件在传递到UIWindow依然无法处理,会继续上传到UIApplication,然后是delegate。appDelegate是完整响应链条的最后一环,如果事件在appDelegate出依然得不到处理,就会走消息派发。