美文网首页事件的产生、传递
事件传递、响应者链条

事件传递、响应者链条

作者: CoderZb | 来源:发表于2016-08-04 19:49 被阅读266次

    UITouch的解释及作用

    • 一根手指对应一个UITouch对象
    • 当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象
    • 保存着跟手指相关的信息,比如触摸的位置、时间、阶段
    • 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
    • 当手指离开屏幕时,系统会销毁相应的UITouch对象

    UITouch的方法

      - (CGPoint)locationInView:(UIView *)view;
    返回值表示触摸在view上的位置
    这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
    调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
    
    - (CGPoint)previousLocationInView:(UIView *)view;
    该方法记录了前一个触摸点的位置
    
    

    UITouch的属性

    触摸产生时所处的窗口
    @property(nonatomic,readonly,retain) UIWindow    *window;
    
    触摸产生时所处的视图
    @property(nonatomic,readonly,retain) UIView      *view;
    
    

    UIEvent的解释及作用

    • 每产生一个事件,就会产生一个UIEvent对象

    • UIEvent:称为事件对象,记录事件产生的时刻和类型

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


    UIEvent的属性

    @property(nonatomic,readonly) UIEventType     type;
    @property(nonatomic,readonly) UIEventSubtype  subtype;
    @property(nonatomic,readonly) NSTimeInterval  timestamp;
    
    

    touches和event参数

    一次完整的触摸过程,会经历3个状态:
    触摸开始:- (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
    
    
    • 4个触摸事件处理方法中,都有NSSet *touches和UIEvent *event两个参数

    • 一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数

    • 如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象

    • 如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象

    • 根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸


    事件的产生和传递

    • 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中

    • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)

      • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步

      • 找到合适的视图控件后,就会调用视图控件的touches方法( touchesBegan…
        touchesMoved…
        touchedEnded…)来作具体的事件处理


    触摸事件处理的详细过程

    • 用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件

    • 找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理(touchesBegan…,touchesMoved…,touchedEnded…)

    • touchesBegan方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理


    响应者对象

    • 在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”(注意:之前说的类对象其实就是类,例如UIImageView类对象,其实就是UIImageView类,而不是UIImageView类创建出来的对象)
    • UIApplication、UIViewController、UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件

    UIResponder内部提供了以下方法来处理事件.因为UIApplication、UIViewController、UIView都继承UIResponder,所以这三个都能拥有以下方法(事件)
    触摸事件
    - (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;
    
    加速计事件
    - (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不接收触摸事件的三种情况

    • 1:userInteractionEnabled = NO(不接收用户交互)
    • 2:hidden = YES(隐藏)
    • 3:alpha = 0.0 ~ 0.01(透明)
    • 提示:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的

    触摸事件的传递

    • 触摸事件的传递是从父控件传递到子控件(最里面的就是父控件,最外面的就是子控件)
      • 点击了绿色的view,事件传递的过程:UIApplication -> UIWindow -> 白色 -> 绿色
      • 点击了蓝色的view,事件传递的过程:UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色
      • 点击了黄色的view,事件传递的过程:UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色 -> 黄色
    • 触摸事件的传递过程中,如果父控件不能接收触摸事件(上面的1,2,3),那么子控件也不可能接收到触摸事件(掌握)。
      • 这种情况接收到触摸事件的只有父控件的父控件。例如蓝色userInteractionEnabled = NO,那么子控件黄色就不能接收到触摸事件,此时点击黄色区域,提示橙色被点击,而不是提示黄色被点击。
      • 详细解释:点击黄色,触摸事件的传递过程是从父控件到子控件,当传递到橙色时,橙色要传递给蓝色,因为蓝色不接收用户交互,所以触摸事件就在经过橙色view停止,不再进行传递。
    • 如果父控件隐藏,那么里面的子控件也跟着隐藏
    • 如果父控件透明(alpha),那么子控件也跟着透明。不过在storyboard中看不出子控件也跟着透明,运行时才会看到

    • 如何找到最合适的控件来处理事件(利用事件的传递过程)?
      • A:自己是否能接收触摸事件?
      • B:触摸点是否在自己身上?(触摸点在子控件身上,若这个子控件有父控件,那么这个触摸点也在父控件身上)
      • C:从后往前遍历子控件,重复前面的两个步骤
      • D:如果没有符合条件的子控件,那么就自己最适合处理

    事例+附图

    • 点击(触摸)黄色的View,找到最合适的控件来处理事件的详细步骤:
      • UIApplication从队列当中取出事件
      • 事件先传递给UIWindow
      • UIWindow满足A,B,此时事件传递给控制器的白色View
      • 白色的View满足A,B,此时白色的View把事件传递给橙色View(因为白色View子控件是绿色和橙色,在storyboard中,橙色是后添加的,后添加的先遍历)
      • 橙色的View满足A,B,此时橙色的view把事件传递给红色的View
      • 红色的View满足A,不满足B。然后橙色的view把事件传递给蓝色的View
      • 蓝色的View满足A,B。蓝色的View把事件传递给黄色的View
      • 黄色的View满足A,B,但是没有子控件,所以C,D不满足,最终蓝色的View来处理事件,所以执行蓝色的View点击
    16-02.png
    16-03.png
    16-04.png

    精华总结1:

    • 判断点在不在白色的View身上,一定要以白色View的左上角为坐标原点,不能以父控件(红色View)的左上角为原点判断,
    • 判断依据:判断点A(x,y)是否大于当前View(白色View)的宽度和高度,如果有一个不满足,则点不在当前的View上。例如如果A的坐标为(15,4),而白色View的宽高都为28,所以满足条件,可得知A点在白色View上
    • 如果以红色View的左上角为坐标原点,判断当前点A是在白色View上的话,A点的坐标为(50,30),而白色的View的宽高都为28,所以不满足条件,由此判断A点不在白色View。
      -判断点在不在白色View身上的官方语言是判断当前点在不在方法调用者身上。就是说,当前方法的调用者:白色View
    • 精华:hitTest方法内部实现了A,B,C,D(详细看下面的代码)。 所以说方法的调用者就是 执行白色View的hitTest方法

    精华总结2(结合总结1):

    • 事件传递到红色View时,执行红色view的hitTest方法,hitTest方法里面的A,B,C,执行到C,从后往前遍历子控件,子控件就是白色View,执行白色View(截图16-06)的hitTest方法,参数中的point是父控件红色View的传递过来的坐标,所以传递过来的point要转换成当前方法所在的View的piont

    • 转换规则:当前点的位置不变,变得只是坐标原点(父控件的坐标原点转成了子控件的坐标原点。)然后计算点的位置距离这个子控件的x,y坐标,这个坐标就是这个点在子控件的point,如果此时point大于这个子控件的宽和高,那么point不在这个子控件上
      代码方式转换:如16-06

    • 代码截图(结合 总结1,总结2)

    16-06.png
    • 详细图解 (结合 总结1,总结2)
    16-05.png

    HitTest方法底层实现

    • 精华:hitTest方法内部实现了A,B,C,D
    //什么时候调用:当一个事件传递给当前View时调用
    //作用:寻找最适合的View
    //返回值:找到的最适合的View,如果没有找到最适合的View,返回是一个nil.
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
        //1.判断当前View能否接收事件
        if(self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
            return nil;
        }
        //2.判断当前点在不在View身上
        if (![self pointInside:point withEvent:event]) {
            return nil;
        }
        //3.从后往前遍历自己的子控件.让自己的子控件寻找最适合的View;
        int count = (int)self.subviews.count;
        for (int i = count - 1; i >= 0; i--) {
            //取出子控件
            UIView *childV = self.subviews[i];
            //把当前点的坐标(父控件)系转换成子控件身上的坐标系
            CGPoint childP = [self convertPoint:point toView:childV];
            UIView *fitView = [childV hitTest:childP withEvent:event];
            if (fitView) {
                return fitView;
            }
        }
        //4.没有找到比自己更适合的view,那么它自己就是最适合的view
        return self;
        
        
    }
    //什么时候调用:在hitTest方法当中调用
    //作用:判断当前点在不在方法调用者身上.
    //返回值:如果返回yes,在View身,如果 为NO,不在View身上.
    //传入的点point,必须得与方法调用者在同一个坐标系(即point是父控件中的坐标,必须转换成子控件身上的坐标,即坐标原点要由父控件变成子控件的坐标原点,唯一不变的是坐标代表的距离不变,这个距离要和当前子控件的宽高进行比较,如果都小于的话,才能证明这个带你在子控件身上).
    //- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    //    return YES;
    //}
    @end
    

    hitTest方法练习


    CharBtn.h文件

    #import <UIKit/UIKit.h>
    
    @interface ChatBtn : UIButton
    
    /** <#注释#>*/
    @property (nonatomic, weak) UIButton *popBtn;
    
    @end
    
    

    CharBtn.m文件

    #import "ChatBtn.h"
    
    @implementation ChatBtn
    //当第一次点击"弹出对话框"时,按照事件的传递过程:APPLication--->UIWindow--->"弹出对话框"按钮,执行到"弹出对话框"按钮时候,所以执行"弹出对话框"重写的hitTest方法(重写只为拦截之用,但还是会执行btnclick方法),在这个方法里面进行判断(A,B,C,D步骤),结果返回系统的做法return [super hitTest:point withEvent:event];所以跳出hitTest方法之后,就会执行btnclick方法.在里面创建一个btn控件并添加到父控件"弹出对话框",并把这个控件赋值给ChatBtn类型的对象sender的popBtn属性,从而popBtn就拿到了btn,拥有了btn的图片以及尺寸存储在内存中
    //第一次点击"弹出对话框"时打印两行null的原因: 第一行:因为没有给self.popBtn赋值,所以self.popBtn为null.第二行:self.popBtn为null,返回的是一个方法,所以再次执行这个重写的方法就返回了,打印第二行null。
    
    //第二次点击"对话框",按照事件的传递过程:APPLication--->UIWindow--->"弹出对话框"按钮,执行到"弹出对话框"按钮时候,"弹出对话框"的hitTest:方法同样会进行拦截(虽然点击的是"对话框",但是因为事件是有传递过程的,每传递到一个控件时,都会执行控件的hitTest方法),self.popBtn此时已经被对话框(小孩)btn赋值,所以就会执行return self.popBtn;就会执行“对话框”按钮的响应事件,即点击“对话框”按钮有反应(popBtn里面之前已经存储了不惦记btn的图片和点击btn时图片的状态以及尺寸等).注意:不会来到btnClick方法,因为你点击的是"对话框“按钮(点在对话框"按钮身上).如果返回的是系统原有的做法return [super hitTest:point withEvent:event];,那肯定是执行btnClick方法(就相当于没重写htiTest方法,即:点击谁,就执行谁的btnClick)。
    
    
    //如果 一个子控件超出父控件的大小, 默认情况下是不能处理的事件.
    //点击"弹出对话框"按钮的子控件是 "对话框"按钮.  并且子控件超出了父控件的范围
    
    //精华:点击对话框按钮,事件传递过程:APPLication--->UIWindow--->按钮,执行到按钮时,在按钮的hitTest方法中先判断出按钮能接收事件,但是点不在按钮身上(因为子控件超出了父控件的大小,并且当前点是父控件未包含的区域,)所以不会执行第三步(不会从后往前遍历按钮的子控件,),就会执行第四步:没有符合条件的子控件,那么就自己最适合处理,所以"弹出对话框"按钮自己处理
    
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        //通过sender.popBtn = btn;拿到对话框按钮btn,在这个类中用self.popBtn表示
        //point是点击"对话框"按钮中的某个点(当然第一次点击"弹出对话框"时,point指的就是那时的点,第二次点击时,才是“对话框”按钮的点)
        
        
         NSLog(@"%@--",self.popBtn);
        //如果点在自己的子控件上面.让子控件处理事件.
        if (self.popBtn) {
            //把当前点转换成"对话框"坐标系上面的点
            CGPoint popBtnP = [self convertPoint:point toView:self.popBtn];
            if ( [self.popBtn pointInside:popBtnP withEvent:event]) {//判断点在不在对话框"按钮身上
                return self.popBtn;
            }else {
                return [super hitTest:point withEvent:event];//点不在对话框"按钮身上,保持系统原有的做法
            }
        }
        
        return   [super hitTest:point withEvent:event];
    }
    
    //
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        //获取UITouch
        UITouch *touch = [touches anyObject];
        //获取当前手指的点
        CGPoint curP = [touch locationInView:self];
        //获取上一个手指所在的点
        CGPoint preP = [touch previousLocationInView:self];
        //计算偏移量
        CGFloat offsetX = curP.x - preP.x;
        CGFloat offsetY =curP.y - preP.y;
        //平移
        self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
    }
    
    
    
    @end
    

    ViewController.h文件

    #import <UIKit/UIKit.h>
    
    @interface ViewController : UIViewController
    
    
    @end
    
    

    ViewController.m文件

    #import "ViewController.h"
    #import "ChatBtn.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    //重:因为"弹出对话框"按钮已经和ChatBtn绑定,所以点击"弹出对话框",传递的sender的类型是CharBtn类型
    - (IBAction)btnClick:(ChatBtn *)sender {
        
        //添加一个子控件
        UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
        [btn setImage:[UIImage imageNamed:@"对话框"] forState:UIControlStateNormal];
        //UIControlStateHighlighted可以添加图片,UIControlStateSelected不可以添加图片,只能被选中
        [btn setImage:[UIImage imageNamed:@"小孩"] forState:UIControlStateHighlighted];
        btn.frame = CGRectMake(50, -80, 100, 80);
        //btn是UIButton类型的指针,popBtn也是UIButton类型的指针,所以可以赋值
        sender.popBtn =btn;
        //sender.popBtn =(ChatBtn *)btn;
        
        [sender addSubview:btn];
    }
    @end
    

    Main.storyboard文件

    16-08.png

    触摸事件处理的详细过程

    • 用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件

    • 找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理(touchesBegan…,touchesMoved…,touchedEnded…)

    • touchesBegan方法的默认做法是将事件顺着响应者链条向上传递(父类),将事件交给上一个响应者进行处理


    事件响应代码

    • 上一个响应者
      • 1.点击当前的View,如果当前的View是控制器的View.那么它的上一个响应者就是所在的控制器
      • 2.如果不是控制器View,那么它的上一个响应者就是它的父控件
    //重写系统的方法--->保持系统默认做法
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
        [super touchesBegan:touches withEvent:event];//保持系统默认做法
    }
    
    
    //重写系统的方法--->处理事件,没有保持系统默认做法
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"%@---touch",[self class]);   //处理事件(打印当前类),没有保持系统默认做法
     
    
    }
    

    //重写系统的方法--->处理事件,并保持系统默认做法
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"%@---touch",[self class]);//处理事件(打印当前类)
        [super touchesBegan:touches withEvent:event];//保持系统默认做法,即顺着响应者链条传递给上一个响应者(父类)。
    }
    
    • E:左侧的图:事件传递到了绿色view
      • 如果绿色view不处理(事件),就会保持系统默认的做法,沿着响应者链条(1)向上传递给黄色View
      • 如果黄色view也不处理(事件),就会保持系统默认的做法,沿着响应者链条(2)向上传递给青色View(控制器的view)。
      • 如果青色view(控制器的view)也不处理(事件),就会保持系统默认的做法,沿着响应者链条(3)向上传递给View Controlleer控制器
      • 如果View Controlleer控制器也不处理事件,就会保持系统默认的做法,沿着响应者链条(4)向上传递给window(窗口)
      • 如果window(窗口)也不处理事件,就会保持系统默认的做法,沿着响应者链条(5)向上传递给Application
      • 此时Application发现没有人处理事件,那么这个事件就会被抛弃掉

    • F:右侧的图:事件传递到了绿色view
      • 如果绿色view不处理(事件),就会保持系统默认的做法,沿着响应者链条(1)向上传递给黄色View(子控制器的view)
      • 如果黄色view(子控制器的view)也不处理,就会保持系统默认的做法,沿着响应者链条(2)向上传递给iew Controlleer控制器
      • 如果View Controlleer控制器也不处理事件,就会保持系统默认的做法,沿着响应者链条(3)向上传递给青色view(父控制器的view)
      • 如果青色view(父控制器的view)也不处理(事件),就会保持系统默认的做法,沿着响应者链条向上传递给View Controlleer控制器
      • 如果View Controlleer控制器也不处理事件,就会保持系统默认的做法,沿着响应者链条(4)向上传递window(窗口)
      • 如果window(窗口)也不处理事件,就会保持系统默认的做法,沿着响应者链条(5)向上传递给Application
      • 此时Application发现没有人处理事件,那么这个事件就会被抛弃掉

    事件的传递和响应过程(看ABCDEF): 传递:在hitTest方法中找到适合的view(A,B,C,D) 响应:在toucherBegan方法中处理事件(E或F)

    16-07.png

    UIGestureRecognizer(手势识别器)

    • UIGestureRecognizer是一个抽象类,定义了所有手势的基本行为,使用它的子类才能处理具体的手势
      • UITapGestureRecognizer(敲击)
      • UIPinchGestureRecognizer(捏合,用于缩放)
      • UIPanGestureRecognizer(拖拽)
      • UISwipeGestureRecognizer(轻扫)
      • UIRotationGestureRecognizer(旋转)
      • UILongPressGestureRecognizer(长按)
    • 如果将手势识别的代理方法(下面的方法):支持多个手势设置为NO,那么系统默认只能识别一个手势.
      • 例如:果你添加了2个手势(旋转,缩放)到代码中,当你运行时,旋转图片的时候,不能缩放, 缩放图片的时候不能旋转
    //是否允许支持多个手势
    -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    
        return NO;
    }
    

    
    //旋转,缩放,位移中:  
    //第一个参数是上次(之前)的位置
    //第二个参数(第三个参数)是距离上次(之前)位置的偏移量
    //等号左边的self.imageV.transform是(这次)现在的位置(现在的位置是根据右边的参数计算出来的位置)
    
    //旋转
     self.imageV.transform = CGAffineTransformRotate(self.imageV.transform, rotation.rotation);
     
     //缩放
     self.imageV.transform = CGAffineTransformScale(self.imageV.transform, pinch.scale, pinch.scale);
     
     //位移
     self.imageV.transform = CGAffineTransformTranslate(self.imageV.transform, transP.x, transP.y);
    

    手势识别

    #import "ViewController.h"
    
    @interface ViewController ()<UIGestureRecognizerDelegate>
    @property (weak, nonatomic) IBOutlet UIImageView *imageV;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
       self.imageV.userInteractionEnabled = YES;
    
        [self panGes];
        
    }
    
    //是否允许支持多个手势
    -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    
        return NO;
    }
    
    //拖动手势
    - (void)panGes {
        //1.创建手势
        UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
        //2.添加手势
        [self.imageV addGestureRecognizer:pan];
        
    }
    
    
    //当拖动时调用
    - (void)pan:(UIPanGestureRecognizer *)pan {
        
        //NSLog(@"%s",__func__);
        //获取偏移量(相对于最原始的值),用transP表示。例如偏移量为100(相对于0来说)(其实是有x和y值)
        //pan.view是当前手势的所在view。所在的view是UIImageView。所以pan.view等价于self.imageV;
        CGPoint transP = [pan translationInView:pan.view];
        NSLog(@"transP=%@",NSStringFromCGPoint(transP));//1
    
        //CGAffineTransformTranslate的第一个参数是记录上次的位置,第二个参数和第三个参数是偏移量x和偏移量y,只有知道上次的位置和偏移量,才能知道下次的位置
        //如果屏蔽2的代码,当你拖动图片的时候,其实系统底层已经记录了你拖动到了什么位置,只是不显示。通过1的打印就可以知道,只是我们通过2将拖动到的位置显示了出来,仅此而已
        self.imageV.transform = CGAffineTransformTranslate(self.imageV.transform, transP.x, transP.y);//2
        
    //复位(相对于上一次的操作) ,把偏移量为1进行清零,那么下次获取偏移量时,就相对于0来说,如果不清零,下次真实的偏移量若为200时,就会走100+200=300的距离,但是实际的偏移量为200
        [pan setTranslation:CGPointZero inView:pan.view];
    }
    
    
    @end
    

    相关文章

      网友评论

        本文标题:事件传递、响应者链条

        本文链接:https://www.haomeiwen.com/subject/vkzcsttx.html