如何让触摸可见

前言

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

图1
图1

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

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

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

图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 ~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,679评论 0 9
  • 在iOS开发中经常会涉及到触摸事件。本想自己总结一下,但是遇到了这篇文章,感觉总结的已经很到位,特此转载。作者:L...
    WQ_UESTC阅读 5,988评论 4 26
  • 好奇触摸事件是如何从屏幕转移到APP内的?困惑于Cell怎么突然不能点击了?纠结于如何实现这个奇葩响应需求?亦或是...
    Lotheve阅读 56,660评论 51 597
  • 在开发过程中,大家或多或少的都会碰到令人头疼的手势冲突问题,正好前两天碰到一个类似的bug,于是借着这个机会了解了...
    闫仕伟阅读 5,294评论 2 23
  • 在面试中,我们经常会遇到一些原理性的问题,很常识但很难用通俗的语言解释清楚,这也是大部分业务级程序员经常失误的地方...
    欧巴冰冰阅读 1,906评论 2 21