如何让触摸可见

作者: Andy__M | 来源:发表于2016-04-16 15:25 被阅读304次

    前言

    平时工作之余,我喜欢到如 cocoachinacode4App 等各大开源库网站逛逛,看看有什么新的、实用的、强大的库可以学习、使用一下,经常能看到效果预览图中的“触摸”(如图1)。

    图2

    进入正题

    出于上述考虑,我开发了一个UIWindow+AMKVisibleTouches的库,在使用时只需引入头文件,然后设置self.window.amk_touchesVisible = YES;,一句话即可达到效果,下面我来介绍下我的实现过程~~

    原理

    其实实现的原理如下

    1. 使用runtime,给UIWindow添加相关属性
    2. 通过Method Swizzling,重新实现UIWindowsendEvent:方法
    3. sendEvent:方法中,获取每一个UITouch *touch对象,为其添加一个视图到window上来代表它,并在该touch移动时更新视图的位置以跟随触摸

    是不是很简单呢?

    runtime & Method Swizzling

    OC是一门运行时语言,通常会与Method Swizzling配合使用,第一次接触还是在公司,我的导师“德芙”大神做《JSPatch介绍与运用之HotFix》技术分享会上,那时就已经见识到runtime的强大和Method Swizzling的变态了,并顶礼膜拜之,
    总之就是runtime为OC带来了无限的可能。
    具体介绍不是本文主要内容,网上的文章也很多,在此就不再赘述了。

    开始开发

    注:开发过程中用到了NSObject+AMKMethodSwizzling,主要提供了如下2个类方法,具体实现可点我查看

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    @interface NSObject (AMKMethodSwizzling)
    /// 交换实例方法
    + (BOOL)amk_swizzleInstanceMethod:(SEL)originalSelector with:(SEL)newSelector;
    /// 交换类方法
    + (BOOL)amk_swizzleClassMethod:(SEL)originalSelector with:(SEL)newSelector;
    @end
    

    首先我们创建一个UIWindow的category,并在.h文件中为其添加一个属性,用来控制“触摸可见”功能的开关

    //
    //  UIWindow+AMKVisibleTouches.h
    //  AMKitLab
    //
    //  Created by Andy__M on 16/4/16.
    //  Copyright © 2016年 Andy__M. All rights reserved.
    //
    
    #import <UIKit/UIKit.h>
    
    /// 可视化触摸
    @interface UIWindow (AMKVisibleTouches)
    @property(nonatomic, assign) BOOL amk_touchesVisible;               //!< 触摸是否可见
    @end
    
    

    因为我们要重新实现UIWindowsendEvent:方法,所以我们要添加自己的amk_sendEvent:并替换系统的方法

    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [UIWindow amk_swizzleInstanceMethod:@selector(sendEvent:) with:@selector(amk_sendEvent:)];
        });
    }
    

    sendEvent:是用来处理UIWindow的触摸与交互的,所以我们可以获取每一个UITouch *touch对象,为其添加一个视图到window上来代表它,并在该touch移动时更新视图的位置以跟随触摸,具体逻辑如下:

    - (void)amk_sendEvent:(UIEvent *)event {
        //  获取所有的触摸对象
        NSSet *allTouches = [event allTouches];
        
        //  为每一个触摸添加圆点视图
        for (UITouch *touch in [allTouches allObjects]) {
            switch (touch.phase) {
                case UITouchPhaseBegan: {   //  触摸开始
                    //  创建一个触摸原点视图              
                    //  设置该触摸视图的tag值为触摸的hash值,方便在之后该触摸移动时通过tag找到该触摸的视图并修改其位置,最后将其添加到视图上
                    break;
                }
                case UITouchPhaseMoved: {   //  触摸移动
                    //  获取该触摸的圆点视图,修改其位置为触摸的位置
                case UITouchPhaseStationary: {  //  当有多个同时触摸时,有的在移动,而另外没移动的触摸会处于UITouchPhaseStationary状态
                    //  获取该触摸的圆点视图,修改其位置为触摸的位置
                    break;
                }
                case UITouchPhaseEnded:         //  触摸结束
                case UITouchPhaseCancelled: {   //  触摸被取消了
                    //  获取该触摸的圆点视图,设置其tag为初始值0,并以动画淡出视图并移除
                    break;
                }
            }
        }
        
        //  调用回系统的实现
        [self amk_sendEvent:event];
    }
    

    因为在整个过程中会用到大量的视图来“使触摸可见”,上文提到的COSTouchVisualizerWindow,就是因为每一个触摸视图与轨迹视图都是新创建的(一次触摸创建的视图最多可达几百个),从而导致其运行不流畅,为了节省资源、提高效率,我们使用复用池的来帮我们管理这些视图,优化之后的逻辑如下:

    - (void)amk_sendEvent:(UIEvent *)event {
        //  获取所有的触摸对象
        NSSet *allTouches = [event allTouches];
        
        //  为每一个触摸添加圆点视图
        for (UITouch *touch in [allTouches allObjects]) {
            switch (touch.phase) {
                case UITouchPhaseBegan: {   //  触摸开始
                    //  从复用池中取出一个触摸视图:若有则将其从复用池中移除,否则创建一个                
                    //  设置该触摸视图的tag值为触摸的hash值,方便在之后该触摸移动时通过tag找到该触摸的视图并修改其位置,最后将其添加到视图上
                    break;
                }
                case UITouchPhaseMoved: {   //  触摸移动
                    //  获取该触摸的圆点视图,修改其位置为触摸的位置
                    //  从复用池中取出一个触摸波纹视图,若有则将其从复用池中移除,否则创建一个
                    //  设置该触摸波纹视图的位置为触摸的位置,最后将其添加到视图上,并以动画使其淡出并放回复用池中
                case UITouchPhaseStationary: {  //  当有多个同时触摸时,有的在移动,而另外没移动的触摸会处于UITouchPhaseStationary状态
                    //  获取该触摸的圆点视图,修改其位置为触摸的位置
                    break;
                }
                case UITouchPhaseEnded:         //  触摸结束
                case UITouchPhaseCancelled: {   //  触摸被取消了
                    //  获取该触摸的圆点视图,设置其tag为初始值0,并以动画淡出视图并移除,最后将该触摸原点视图放回复用池
                    break;
                }
            }
        }
        
        //  调用回系统的实现
        [self amk_sendEvent:event];
    }
    

    为了能更好的展示“触摸”,我们可以在window上添加一个视图,用于承载触摸视图,综上所述,我们要在.m中声明如下的私有属性:

    @interface UIWindow ()
    @property(nonatomic, strong) NSMutableSet<AMTouchView *> *touchViewReusePool;                   //!< 触摸点视图的复用池
    @property(nonatomic, strong) NSMutableSet<AMTouchRippleView *> *touchRippleViewReusePool;       //!< 触摸波纹视图的复用池
    @property(nonatomic, strong) UIView *touchContainerView;                                        //!< 触摸点容器视图
    @end
    

    .m的实现如下:
    (注:所有用到的新添加的属性都是通过懒加载的方式创建)

    //
    //  UIWindow+AMKVisibleTouches.m
    //  AMKitLab
    //
    //  Created by Andy__M on 16/4/16.
    //  Copyright © 2016年 Andy__M. All rights reserved.
    //
    
    #import "UIWindow+AMKVisibleTouches.h"
    #import "NSObject+AMKMethodSwizzling.h"
    
    /// 触摸视图
    @interface AMTouchView : UIView @end
    
    @implementation AMTouchView
    -(instancetype)init {
        return [self initWithFrame:CGRectMake(0, 0, 50, 50)];
        
    }
    
    - (instancetype)initWithFrame:(CGRect)frame {
        if (self = [super initWithFrame:frame]) {
            self.backgroundColor = [UIColor colorWithWhite:0.916 alpha:1.000];
            self.layer.borderColor = [UIColor lightGrayColor].CGColor;
            self.layer.borderWidth = 1;
            self.layer.cornerRadius = self.frame.size.width / 2;
            self.layer.shadowColor = [UIColor blackColor].CGColor;
            self.layer.shadowOffset = CGSizeZero;
            self.layer.shadowOpacity = 0.3;
            self.layer.shadowRadius = 5;
        }
        return self;
    }
    @end
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    
    /// 触摸波纹视图
    @interface AMTouchRippleView : UIView @end
    
    @implementation AMTouchRippleView
    
    - (instancetype)init {
        return [self initWithFrame:CGRectMake(0, 0, 50, 50)];
    }
    
    - (instancetype)initWithFrame:(CGRect)frame {
        if (self = [super initWithFrame:frame]) {
            self.backgroundColor = [UIColor colorWithWhite:0.916 alpha:1.000];
            self.layer.cornerRadius = self.frame.size.width / 2;
        }
        return self;
    }
    @end
    
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    static void * UIWINDOW_TOUCHES_VISIBLE_KEY = &UIWINDOW_TOUCHES_VISIBLE_KEY;
    static void * UIWINDOW_TOUCH_VIEW_REUSE_POOL_KEY = &UIWINDOW_TOUCH_VIEW_REUSE_POOL_KEY;
    static void * UIWINDOW_TOUCH_RIPPLE_VIEW_REUSE_POOL_KEY = &UIWINDOW_TOUCH_RIPPLE_VIEW_REUSE_POOL_KEY;
    static void * UIWINDOW_TOUCH_CONTAINER_VIEW_KEY = &UIWINDOW_TOUCH_CONTAINER_VIEW_KEY;
    
    @interface UIWindow ()
    @property(nonatomic, strong) NSMutableSet<AMTouchView *> *touchViewReusePool;                   //!< 触摸点视图的复用池
    @property(nonatomic, strong) NSMutableSet<AMTouchRippleView *> *touchRippleViewReusePool;       //!< 触摸波纹视图的复用池
    @property(nonatomic, strong) UIView *touchContainerView;                                        //!< 触摸点容器视图
    @end
    
    @implementation UIWindow (AMKVisibleTouches)
    
    #pragma mark - Life Circle
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [UIWindow amk_swizzleInstanceMethod:@selector(sendEvent:) with:@selector(amk_sendEvent:)];
            [UIWindow amk_swizzleInstanceMethod:@selector(layoutSubviews) with:@selector(amk_layoutSubviews)];
        });
    }
    
    #pragma mark - Propertys
    
    - (BOOL)amk_touchesVisible {
        NSNumber *touchesVisible = objc_getAssociatedObject(self, UIWINDOW_TOUCHES_VISIBLE_KEY);
        return  (touchesVisible)?([touchesVisible boolValue]):NO;
    }
    
    - (void)setAmk_touchesVisible:(BOOL)touchesVisible {
        objc_setAssociatedObject(self,UIWINDOW_TOUCHES_VISIBLE_KEY, @(touchesVisible), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (NSMutableSet<AMTouchView *> *)touchViewReusePool {
        NSMutableSet *touchViewReusePool = objc_getAssociatedObject(self, UIWINDOW_TOUCH_VIEW_REUSE_POOL_KEY);
    
        if (!touchViewReusePool) {
            touchViewReusePool = [NSMutableSet set];
            self.touchViewReusePool = touchViewReusePool;
        }
        return touchViewReusePool;
    }
    
    - (void)setTouchViewReusePool:(NSMutableSet<AMTouchView *> *)touchViewReusePool {
        objc_setAssociatedObject(self,UIWINDOW_TOUCH_VIEW_REUSE_POOL_KEY, touchViewReusePool, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (NSMutableSet<AMTouchRippleView *> *)touchRippleViewReusePool {
        NSMutableSet *touchRippleViewReusePool = objc_getAssociatedObject(self, UIWINDOW_TOUCH_RIPPLE_VIEW_REUSE_POOL_KEY);
        
        if (!touchRippleViewReusePool) {
            touchRippleViewReusePool = [NSMutableSet set];
            self.touchRippleViewReusePool = touchRippleViewReusePool;
        }
        return touchRippleViewReusePool;
    }
    
    - (void)setTouchRippleViewReusePool:(NSMutableSet<AMTouchRippleView *> *)touchRippleViewReusePool {
        objc_setAssociatedObject(self,UIWINDOW_TOUCH_RIPPLE_VIEW_REUSE_POOL_KEY, touchRippleViewReusePool, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (UIView *)touchContainerView {
        UIView *touchContainerView = objc_getAssociatedObject(self, UIWINDOW_TOUCH_CONTAINER_VIEW_KEY);
        
        if (!touchContainerView) {
            touchContainerView = [[UIView alloc] initWithFrame:self.bounds];
            touchContainerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
            touchContainerView.backgroundColor = [UIColor clearColor];
            touchContainerView.alpha = 0.5;
            touchContainerView.userInteractionEnabled = NO;
            [self addSubview:touchContainerView];
            self.touchContainerView = touchContainerView;
        }
        return touchContainerView;
    }
    
    - (void)setTouchContainerView:(UIView *)touchContainerView {
        objc_setAssociatedObject(self,UIWINDOW_TOUCH_CONTAINER_VIEW_KEY, touchContainerView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    #pragma mark - Actions
    
    - (void)amk_sendEvent:(UIEvent *)event {
        //  获取所有的触摸对象
        NSSet *allTouches = [event allTouches];
        
        //  为每一个触摸添加圆点视图
        for (UITouch *touch in [allTouches allObjects]) {
            AMTouchView *touchView;
            switch (touch.phase) {
                case UITouchPhaseBegan: {   //  触摸开始
                    //  从复用池中取出一个触摸视图:若有则将其从复用池中移除,否则创建一个
                    touchView = self.touchViewReusePool.anyObject;
                    if (touchView) {
                        [self.touchViewReusePool removeObject:touchView];
                    } else {
                        touchView = [[AMTouchView alloc] init];
                    }
                    
                    //  设置该触摸视图的tag值为触摸的hash值,方便在之后该触摸移动时通过tag找到该触摸的视图并修改其位置,最后将其添加到视图上
                    touchView.tag = touch.hash;
                    touchView.center = [touch locationInView:self.touchContainerView];
                    [self.touchContainerView addSubview:touchView];
                    break;
                }
                case UITouchPhaseMoved: {   //  触摸移动
                    //  获取该触摸的圆点视图,修改其位置为触摸的位置
                    if (!touchView) touchView = (AMTouchView *)[self.touchContainerView viewWithTag:touch.hash];
                    touchView.center = [touch locationInView:self.touchContainerView];
                    
                    //  从复用池中取出一个触摸波纹视图,若有则将其从复用池中移除,否则创建一个
                    AMTouchRippleView *touchRippleView = self.touchRippleViewReusePool.anyObject;
                    if (touchRippleView) {
                        [self.touchRippleViewReusePool removeObject:touchRippleView];
                    } else {
                        touchRippleView = [[AMTouchRippleView alloc] init];
                    }
                    
                    //  设置该触摸波纹视图的位置为触摸的位置,最后将其添加到视图上,并以动画使其淡出并放回复用池中
                    touchRippleView.center = [touch locationInView:self.touchContainerView];
                    [self.touchContainerView insertSubview:touchRippleView belowSubview:touchView];
                    [UIView animateWithDuration:0.4 animations:^{
                        touchRippleView.alpha = 0;
                        touchRippleView.transform = CGAffineTransformMakeScale(0.2, 0.2);
                    } completion:^(BOOL finished) {
                        [touchRippleView removeFromSuperview];
                        touchRippleView.alpha = 1;
                        touchRippleView.transform = CGAffineTransformIdentity;
                        [self.touchRippleViewReusePool addObject:touchRippleView];
                    }];
                    break;
                }
                case UITouchPhaseStationary: {  //  当有多个同时触摸时,有的在移动,而另外没移动的触摸会处于UITouchPhaseStationary状态
                    //  获取该触摸的圆点视图,修改其位置为触摸的位置
                    if (!touchView) touchView = (AMTouchView *)[self.touchContainerView viewWithTag:touch.hash];
                    touchView.center = [touch locationInView:self.touchContainerView];
                    break;
                }
                case UITouchPhaseEnded:         //  触摸结束
                case UITouchPhaseCancelled: {   //  触摸被取消了
                    //  获取该触摸的圆点视图,设置其tag为初始值0,并以动画淡出视图并移除,最后将该触摸原点视图放回复用池
                    if (!touchView) touchView = (AMTouchView *)[self.touchContainerView viewWithTag:touch.hash];
                    touchView.tag = 0;
                    [UIView animateWithDuration:0.3 animations:^{
                        touchView.alpha = 0;
                    } completion:^(BOOL finished) {
                        [touchView removeFromSuperview];
                        touchView.alpha = 1;
                        [self.touchViewReusePool addObject:touchView];
                    }];
                    break;
                }
            }
        }
        
        //  调用回系统的实现
        [self amk_sendEvent:event];
    }
    
    - (void)amk_layoutSubviews {
        //  先调用一下系统的实现
        [self amk_layoutSubviews];
        //  保持触摸视图在window的最上方显示
        [self bringSubviewToFront:self.touchContainerView];
    }
    
    @end
    
    

    至此,这个库就开发完成了,具体的可以到我的Github上下载Demo来运行查看效果,地址:https://github.com/AndyM129/AMKVisibleTouches ~~

    相关文章

      网友评论

        本文标题:如何让触摸可见

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