如何让触摸可见

前言

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

图1
图1

起初以为是录屏软件的效果,直到看到了 COSTouchVisualizerWindow (效果见图1),才知道这是代码实现的,接入自己的项目试了一下,瞬间就好像哥伦布发现新大陆一般的激动~~

可是使用之余就发现了这个库存在着一些不足,比如:它是一个UIWindow的子类,初始化Window时要改用这个类;在真机上运行有时会有卡顿;它的UI不是很喜欢(虽然有可以设置UI的属性可以修改)。

基于自己对完美的追求(其实就是自己不服气,想要做个更好的_),决定要自己写一个,做到简单易用,无侵入性,结果见图2。

如何让触摸可见_第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 
#import 
@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 

/// 可视化触摸
@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 *touchViewReusePool;                   //!< 触摸点视图的复用池
@property(nonatomic, strong) NSMutableSet *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 *touchViewReusePool;                   //!< 触摸点视图的复用池
@property(nonatomic, strong) NSMutableSet *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 *)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 *)touchViewReusePool {
    objc_setAssociatedObject(self,UIWINDOW_TOUCH_VIEW_REUSE_POOL_KEY, touchViewReusePool, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSMutableSet *)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 *)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 ~~

你可能感兴趣的:(如何让触摸可见)