事件处理

2023-01-04  本文已影响0人  飘摇的水草
iOS事件
响应者对象
UIResponder

UIResponder内部提供了以下方法来处理事件,触摸事件

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withE*ent:(UIEvent *)event;
- (void)touchesCancelted:(NSSet *)ouches withivent:(UIEvent *)event;
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
UIView的触摸事件处理

UIView是 UIResponder 的子类,可以覆盖下列4个方法处理不同的触摸事件

- (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
UITouch

UITouch的属性

@property (nonatomic,readon ly,retain) UIWindow  *window;
@property(nonatomic,readonly,retain) UIView *view;
@property(nonatomic,readonly) NSUInteger tapCount;
@property(nonatomic,readonly) NSTimeInterval timestamp;
@property(nonatomic,readonly) UITouchPhase  phase;

UITouch的方法

- (CGPoint)locationInView:(UIView *)view;

返回值表示触摸在view上的位置
这里返回的位置是针对view的坐标系的(以view的左上角为原点(O,0))调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置

- (CGPoint)previousLocationInView:(UIView *)view;

该方法记录了前一个触摸点的位置

实例

实现如下图所示的效果

在自定义的 View 对象里,代码如下:

@implementation RedView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@",NSStringFromSelector(_cmd));
    
    //获取UITouch对象
    UITouch *touch = [touches anyObject];
    //获取当前点
    CGPoint currentPo = [touch locationInView:self];
    //获取上一个点
    CGPoint previewPo = [touch previousLocationInView:self];
    
    //获取x轴偏移量
    CGFloat offsetX = currentPo.x - previewPo.x;
    
    //获取y轴偏移量
    CGFloat offsetY = currentPo.y - previewPo.y;
    
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);
}

@end
UIEvent

常见属性

事件类型
@property(nonatomic,readonly) UIEventType type
@property(nonatomic,readonly) UIEventSubtype subtype;
事件产生的时间
@property(nonatomic,readonly) NSTimeInterval timestamp;

UIEvent还提供了相应的方法可以获得在某个view土面的触摸对象(UITouch)

当手指触摸屏幕的时候即生成一个 UITouch 对象和 UIEvent 对象

事件的产生和传递

UIView不能接收触摸事件的三种情况

  1. 不接收用户交互:userInteractionEnabled = NO,提示:UIImageView的userInteractionEnabled 默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的。
  2. 隐藏:hidden = YES;
  3. 透明:alpha = 0.0 ~ 0.01;
如何找到最合适的控件来处理事件?
  1. 自己是否能接收触摸事件?
  2. 触摸点是否在自己身上?
  3. 从后往前遍历子控件,重复前面的整个步骤

hitTest底层实现

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //判断当前控件能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01)
    {
        return nil;
    }
    
    //判断点在不在当前控件
    if ([self pointInside:point withEvent:event])
    {
        return nil;
    }
    
    /**自己写的
    //从后往前遍历子控件
    __block UIView *targetView = nil;
    [self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
        CGPoint subPoint = [self convertPoint:point toView:obj];
        UIView *fitView = [obj hitTest:subPoint withEvent:event];
        if (fitView) {
            targetView = fitView;
            *stop = YES;
        }
    }];
    
    if (targetView != nil)
    {
        return targetView;
    }
     */
    
    //从后向前遍历子控件
    for (NSInteger i = [self.subviews count] - 1; i >= 0; i--)
    {
        UIView *subview = self.subviews[i];
        //把当前控件上的坐标系统转换成子控件上的坐标系
        CGPoint subPoint = [self convertPoint:point toView:subview];
        UIView *fitView = [subview hitTest:subPoint withEvent:event];
        if (fitView ) {
            //找到最合适的view
            return fitView;
        }
    }
    return self;
}
hitTest练习1

下面的图片中绿色的图层处于上方,按钮处于下方,当用户点击绿色视图和按钮重叠的区域时,要求响应的是处于下方的按钮。

实现效果.gif
#import "GreenView.h"

@interface GreenView ()
//注意这里从xib或者storyBoad直接拖拽可能不能成功,需要先手写下面这段代码然后反向拖拽对应xib或者storyBoard里的控件
@property (nonatomic, weak) IBOutlet UIButton *button;
@end

@implementation GreenView

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //当前坐标系上的点转换到按钮上的点
    CGPoint btnPoint = [self convertPoint:point toView:self.button];
    //判断点在不在按钮上
    BOOL inside = [self.button pointInside:btnPoint withEvent:event];
    if (inside) {
        return self.button;
    }
    else{
        return [super hitTest:point withEvent:event];
    }
}
@end
hitTest练习2

实现如下场景,要求:

  1. 点击 “弹出对话框” 时出现新的图片
  2. 拖拽 “弹出对话框” 时新的图片也跟着拖动,
  3. 点击新出现的图片时,高亮状态下图片变化

详细的效果请参考下图

hitTest练习2.gif

完整的代码如下所示:

#import <UIKit/UIKit.h>

@interface CustomBtn : UIButton
//出现的新的图片按钮
@property (nonatomic, strong) UIButton *subBtn;
@end


#import "CustomBtn.h"

@implementation CustomBtn

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint position = [touch locationInView:self];
    CGPoint previsePoint = [touch previousLocationInView:self];
    CGFloat xOffset = position.x - previsePoint.x;
    CGFloat yOffset = position.y - previsePoint.y;
    
    CGPoint center = self.center;
    center.x += xOffset;
    center.y += yOffset;
    self.center = center;
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGPoint subPoint = [self convertPoint:point toView:self.subBtn];
    if ([self.subBtn pointInside:subPoint withEvent:event])
    {
        return self.subBtn;
    }
    else{
        return [super hitTest:point withEvent:event];
    }
}

@end

#import "CustomBtn.h"
#import "ThirdViewController.h"

@interface ThirdViewController ()

@end

@implementation ThirdViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
}

//点击 “弹出对话框” 按钮时触发此方法
- (IBAction)customBtnClicked:(CustomBtn *)sender
{
    UIButton *subBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    [subBtn setBackgroundImage:[UIImage imageNamed:@"对话框"] forState:UIControlStateNormal];
    [subBtn setBackgroundImage:[UIImage imageNamed:@"小孩"] forState:UIControlStateHighlighted];
    subBtn.bounds = CGRectMake(0, 0, 150, 150);
    subBtn.center = CGPointMake(75, -75);
    [sender addSubview:subBtn];
    
    sender.subBtn = subBtn;
}
@end
事件的响应

触摸事件处理的详细过程

响应者链条示意图
  1. 先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件。
  2. 调用最合适控件的touches....方法
  3. 如果调用了[super touches....];就会将事件顺着响应者链条往上传递,传递给上一个响应者
  4. 接着就会调上一个响应者的touches....方法

用下面例子来进行演示

响应者链

在该图中,YellowView 的父视图是 BlueView,而 BlueView 的父视图是 OrangeView,依次是 WhiteView,当点击 YellowView 时,会首先检测 YellowView 是否实现了 touches... 等方法,如果其没有实现则事件传给 BlueView ,如果其实现了 touches... 方法,则调用完 touches... 方法,事件结束,如果其不仅实现了 touches... 方法,并且在 touches... 方法内部调用了 [super touches...],则事件继续向上传递给 BlueView ,依次类推,利用这个特性可以做到多个视图触发同一事件。

代码如下:

#import "YellowView.h"

@implementation YellowView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);

    [super touchesBegan:touches withEvent:event];
}

@end

#import "BlueView.h"

@implementation BlueView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);

    [super touchesBegan:touches withEvent:event];
}

@end

#import "OrangeView.h"

@implementation OrangeView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);

    [super touchesBegan:touches withEvent:event];
}

@end


#import "WhiteView.h"

@implementation WhiteView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);
    
    [super touchesBegan:touches withEvent:event];
}

@end

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);
}

@end

当事件传递给白色视图以后,白色视图会继续向上传递给它的父视图,即控制器的view,因为这个视图不处理,则继续传递给当前控制器,输出结果如下所示:

事件传递输出结果
  1. 如果当前这个view是控制器的view,那么 控制器 就是上一个响应者
  2. 如果当前这个view不是控制器的view,那么 父控件 就是上一个响应者
上一篇 下一篇

猜你喜欢

热点阅读