前言
平时工作之余,我喜欢到如 cocoachina、code4App 等各大开源库网站逛逛,看看有什么新的、实用的、强大的库可以学习、使用一下,经常能看到效果预览图中的“触摸”(如图1)。
起初以为是录屏软件的效果,直到看到了 COSTouchVisualizerWindow (效果见图1),才知道这是代码实现的,接入自己的项目试了一下,瞬间就好像哥伦布发现新大陆一般的激动~~
可是使用之余就发现了这个库存在着一些不足,比如:它是一个UIWindow
的子类,初始化Window时要改用这个类;在真机上运行有时会有卡顿;它的UI不是很喜欢(虽然有可以设置UI的属性可以修改)。
基于自己对完美的追求(其实就是自己不服气,想要做个更好的_),决定要自己写一个,做到简单易用,无侵入性,结果见图2。
进入正题
出于上述考虑,我开发了一个UIWindow+AMKVisibleTouches的库,在使用时只需引入头文件,然后设置self.window.amk_touchesVisible = YES;
,一句话即可达到效果,下面我来介绍下我的实现过程~~
原理
其实实现的原理如下
- 使用runtime,给
UIWindow
添加相关属性 - 通过Method Swizzling,重新实现
UIWindow
的sendEvent:
方法 - 在
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
因为我们要重新实现UIWindow
的sendEvent:
方法,所以我们要添加自己的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 ~~