• 图解:习近平出席博鳌亚洲论坛2018年年会前瞻 2019-03-19
  • 外贸创新举措 让开放惠及世界 2019-03-15
  • 记者带你逛CES:体验未来科技 2019-03-14
  • 我的中国医改建议,彻底解决看病难看病贵 2019-03-14
  • 海空联合演练相关新闻 2019-01-05
  • 登录 立即注册
    金钱:

    广州快乐十分开奖记录

    广东快乐十分是否诈骗:谈谈响应链 [复制链接]

    2018-12-5 10:27
    BlueManlove 阅读:323 评论:1 赞:0
    Tag:  

    广州快乐十分开奖记录 www.2hyn.net 当用户的手指在屏幕上的某一点按下时,屏幕接收到点击信号将点击位置转换成具体坐标,然后本次点击被包装成一个点击事件UIEvent。最终会存在某个视图响应本次事件进行处理,而为UIEvent查找响应视图的过程被称为响应链查找,在整个过程中有两个至关重要的类:UIResponder和UIView

    响应者

    响应者是可以处理事件的具体对象,一个响应者应当是UIResponder或其子类的实例对象。从设计上来看,UIResponder主要提供了三类接口:

    • 向上查询响应者的接口,体现在nextResponder这个唯一的接口

    • 用户操作的处理接口,包括touch、press和remote三类事件的处理

    • 是否具备处理action的能力,以及为其找到target的能力

    总体来说UIResponder为整个事件查找过程提供了处理能力

    视图

    视图是展示在界面上的可视元素,包括不限于文本、按钮、图片等可见样式。虽然UIResponder提供了让对象响应事件的处理能力,但拥有处理事件能力的responder是无法被用户观察到的,换句话说,用户也无法点击这些有处理能力的对象,因此UIView提供了一个可视化的载体,从接口上来看UIView提供了三方面的能力:

    • 视图树结构。虽然responder也存在相同的树状结构,但其必须依托于可视载体进行表达

    • 可视化内容。通过frame等属性决定视图在屏幕上的可视范围,提供了点击坐标和响应可视对象的关联能力

    • 内容布局重绘。视图渲染到屏幕上虽然很复杂,但按照不同的layout方式提供了不同阶段的重绘调起接口,使得子类具有很强的定制性

    视图树的结构如下,由于UIView是UIResponder的子类,可以通过nextResponder访问到父级视图,但由于responder并不全是具备可视载体的对象,通过nextResponder向上查找的方式可能会导致无法通过位置计算的方式查找响应者 

    查找响应者

    讲了这么多,也该聊聊查找响应者的过程了。前面说了,responder决定了对象具有响应处理的能力,而UIView才是提供了可视载体和点击坐标关联的能力?;痪浠八?,查找响应者实际上是查找点击坐标落点位置在其可视范围内且其具备处理事件能力的对象,按照官方话来说就是既要responde又要view的对象。因为需要先找到响应者,才能有进一步的处理,所以直接从后者的接口找起,两个api:

    /// 检测坐标点是否落在当前视图范围内
    - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
    /// 查找响应处理事件的最终视图
    - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;

    通过exchange掉第一个方法的实现,很轻易的就能得出响应者查找的顺序:

    - (BOOL)sl_pointInside: (CGPoint)point withEvent: (UIEvent *)event {
        BOOL res = [self sl_pointInside: point withEvent: event];
        if (res) {
            NSLog(@"[%@ can answer]"self.class);
        } else {
            NSLog(@"non answer in %@"self.class);
        }
        return res;
    }

    如图创建相同的布局结构,然后点击BView,得到的日志:

    [UIStatusBarWindow can answer]
    non answer in UIStatusBar
    non answer in UIStatusBarForegroundView
    non answer in UIStatusBarServiceItemView
    non answer in UIStatusBarDataNetworkItemView
    non answer in UIStatusBarBatteryItemView
    non answer in UIStatusBarTimeItemView
    [UIWindow can answer]
    [UIView can answer]
    non answer in CView
    [AView can answer]
    [BView can answer]

    通过日志输出可以看出查找顺序优先级有两条:

    1. 优先级更高的window优先匹配

    2. 同一个window中从父视图向子视图寻找

    通过pointInside:确定了点击坐标落在哪些视图的范围后,会继续调用另一个方法来找寻真正的响应者。同样hook掉这个方法,从日志来看父级视图会调用子视图,最终以递归的格式输出:

    - (UIView *)sl_hitTest: (CGPoint)point withEvent: (UIEvent *)event {
        UIView *res = [self sl_hitTest: point withEvent: event];
        NSLog(@"hit view is: %@ and self is: %@", res.class, self.class);
        return res;
    }

    /// 输出日志
    hit view is: (null) and self is: CView
    hit view is: BView and self is: BView
    hit view is: BView and self is: AView
    hit view is: BView and self is: UIView
    hit view is: BView and self is: UIWindow

    当确定了是CView是最后一个落点视图时,会以CView为起始点,向上寻找事件的响应者,这个查找过程由一个个responder链接起来,这也是响应链名字的来由。另外,基于树结构的视图层级,只需要我们持有根节点,就能遍历整棵树,这也是为什么搜索过程是从window开始的

    响应者处理

    经过pointInside:和hitTest:两个方法会确定点击位置最上方的可视响应者,但并不代表了这个responder会处理本次事件?;谏厦娴慕缑鎑emo,在AView中实现touches方法:

    @implementation AView

    - (void)touchesBegan: (NSSet<UITouch *> *)touches withEvent: (UIEvent *)event {
        NSLog(@"A began");
    }

    - (void)touchesCancelled: (NSSet<UITouch *> *)touches withEvent: (UIEvent *)event {
        NSLog(@"A canceled");
    }

    - (void)touchesEnded: (NSSet<UITouch *> *)touches withEvent: (UIEvent *)event {
        NSLog(@"A ended");
    }

    @end

    前面说过UIResponder提供了用户操作的处理接口,但很明显touches系列的接口默认是未实现的,因此BView即便成为了响应链上的最上层节点,依旧无法处理点击事件,而是沿着响应链查找响应者:

    void handleTouch(UIResponder *responder, NSSet<UITouch *> *touches UIEvent *event) {
        if (!responder) {
            return;
        }
        if ([responder respondsToSelector: @selector(touchesBegan:withEvent:)]) {
            [responder touchesBegan: touches withEvent: event];
        } else {
            handleTouch(responder.nextResponder, touches, event);
        }
    }

    另外一个有趣的地方是手势拦截,除了实现touches系列方法让view提供响应能力之外,我们还可以主动的在视图上添加手势进行回调:

    - (void)viewDidLoad {
        [super viewDidLoad];
        [_a addGestureRecognizer: [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(clickedA:)]];
    }

    - (void)clickedA: (UITapGestureRecognizer *)tap {
        NSLog(@"gesture clicked A");
    }

    /// 日志输出
    A began
    gesture clicked A
    A canceled

    从日志可以看到手势的处理是在touchesBegan之后,并且执行之后会中断原有的touches调用链,因此可以确定即便是手势动作,最终依旧由UIResponder进行事件分发处理

    小结

    触屏事件的处理被分成两个阶段:查找响应者和响应者处理,分别由UIView和 UIResponder提供了功能上的支持。另外由于pointInside:和hitTest:这两个关键接口是对外暴露的,因此通过hook或者inherit的方式来修改这两个方法,可以使得视图的可响应范围大于显示范围

    应用

    最近有个需求需要在tabbar的位置上方弹出气泡,且允许用户点击气泡发生交互事件。从视图层来分析,tabbar被嵌套在多层尺寸等同于菜单栏的view当中:

    如果要实现从item弹起气泡并且可交互,有两个可行的方案:

    1. 将气泡添加到ViewController的视图上

    2. 修改tabbar的响应链查找接口,实现显示范围外的可点击处理

    考虑到如果项目中存在相同的弹出需求可能会导致写了一堆重复代码,所以将弹出动作和触屏判断给封装起来,通过hook的方式来实现弹出气泡自动化触屏检测功能

    接口设计

    依照最少接口原则,只暴露两个弹出接口:

    /*!
     *  @enum   SLViewDirection
     *  弹窗视图所在的方向(dismiss和pop应当保持一致)
     */

    typedef NS_ENUM(NSInteger, SLViewDirection)
    {
        SLViewDirectionCenter,
        SLViewDirectionTop,
        SLViewDirectionLeft,
        SLViewDirectionBottom,
        SLViewDirectionRight
    };

    /*!
     *  @category   UIView+SLFreedomPop
     *  自由弹窗扩展
     */

    @interface UIView (SLFreedomPop)

    /*!
     *  @method sl_popView:
     *  居中弹出view
     *  @param  view    要弹出的view
     */

    - (void)sl_popView: (UIView *)view;

    /*!
     *  @method sl_popView:WithDirection:
     *  控制弹窗方向
     *  @param  view        要弹出的view
     *  @param  direction   弹出方向
     */

    - (void)sl_popView: (UIView *)view withDirection: (SLViewDirection)direction;

    @end


    落点检测

    考虑到两个问题:

    1. 视图可能存在多个超出显示范围的子视图

    2. 视图存在超出父级视图显示范围的子视图

    针对这两个问题,解决方案分别是:

    1. 用视图作key,存储一个extraRect的列表

    2. 当弹出视图超出自身范围时,向父级视图调用,确保父级视图能处理响应

    最终检测代码如下:

    #define SLRectOverflow(subrect, rect) \
        subrect.origin.x < 0 || \
        subrect.origin.y < 0 || \
        CGRectGetMaxX(subrect) > CGRectGetWidth(rect) ||   \
        CGRectGetMaxY(subrect) > CGRectGetHeight(rect)

    #pragma mark - Private
    - (BOOL)_sl_pointInsideExtraRects: (CGPoint)point {
        NSArray *extraRects = [self extraHitRects].allValues;
        if (extraRects.count == 0) {
            return NO;
        }

        for (NSSet *rects in extraRects) {
            for (NSString *rectStr in rects) {
                if (CGRectContainsPoint(CGRectFromString(rectStr), point)) {
                    return YES;
                }
            }
        }
        return NO;
    }

    #pragma mark - Rects
    - (void)_sl_addExtraRect: (CGRect)extraRect inSubview: (UIView *)subview {
        CGRect curRect = [subview convertRect: extraRect toView: self];
        if (SLRectOverflow(curRect, self.frame)) {
            [self _sl_expandExtraRects: curRect forKey: [NSValue valueWithBytes: &subview objCType: @encode(typeof(subview))]];
            [self.superview _sl_addExtraRect: curRect inSubview: self];
        }
    }

    #pragma mark - Hook
    - (BOOL)sl_pointInside: (CGPoint)point withEvent: (UIEvent *)event {
        BOOL res = [self sl_pointInside: point withEvent: event];
        if (!res) {
            return [self _sl_pointInsideExtraRects: point];
        }
        return res;
    }

    - (UIView *)sl_hitTest: (CGPoint)point withEvent: (UIEvent *)event {
        UIView *res = [self sl_hitTest: point withEvent: event];
        if (!res) {
            if ([self _sl_pointInsideExtraRects: point]) {
                return self;
            }
        }
        return res;
    }


    运行效果

    红色视图弹出绿色视图,超出自身和父视图的显示范围,点击有效: 

    定制改进

    由于核心功能在于event handle,目前代码只提供了简单的弹出接口,需要进一步扩充弹出接口能力的可以考虑两点改进:

    • 增加configuration来定制弹窗样式

    • 提供animation来完成自定义的动画

    源码:https://github.com/sindrilin/SLFreedomPop
    分享到:
    我来说两句
    facelist
    您需要登录后才可以评论 登录 | 立即注册
    所有评论(1)
    阳光男孩2号 2018-12-6 11:12
    mark
    回复
    关闭

    每日头条

    通过邮件订阅最新 Code4App 信息
    上一条 /4 下一条
    联系我们
    关闭
    合作电话:
    13802416937
    Email:
    [email protected]
    商务市场合作/投稿
    问题反馈及帮助
    联系我们

    广告投放| 广东互联网违法和不良信息举报中心|中国互联网举报中心|Github|申请友链|手机版|广州快乐十分开奖记录 ( 广州快乐十分开奖记录 )

    返回顶部
  • 图解:习近平出席博鳌亚洲论坛2018年年会前瞻 2019-03-19
  • 外贸创新举措 让开放惠及世界 2019-03-15
  • 记者带你逛CES:体验未来科技 2019-03-14
  • 我的中国医改建议,彻底解决看病难看病贵 2019-03-14
  • 海空联合演练相关新闻 2019-01-05