手势识别器(Gesture Recognizer)用于识别触摸序列并触发响应事件。当手势识别器识别到一个手势或手势发生变化时,会触发响应事件。UIGestureRecognizer类是具体手势识别器的基础类 ,不能直接使用。只能使用UIGestureRecognizer的子类识别手势,每一个子类识别一个特定的手势。UIGestureRecognizer有以下几个子类:
UITapGestureRecognizer:点击手势识别器,手势可以是一次点击或多次点击,可以是一个手指也可以是多个手指。点击手势是最为常用的手势。UISwipeGestureRecognizer:滑动手势识别器,滑动方向可以是上下左右任一方向。滑动手势的典型示例是查看照片的应用程序,我们使用手指从一张照片滑动到另一张照片。UIPanGestureRecognizer:平移手势识别器,也称为拖动手势识别器。当用户平移视图时,必须保持一个或多个手指始终按压在视图上。UIPinchGestureRecognizer:捏合手势识别器,需要两个手指同时触摸视图。当两个手指靠近时,视图缩小;当两根手指远离时,视图放大。UIRotationGestureRecognizer:旋转手势识别器,需要两个手指同时触摸视图。当用户手指彼此相对做圆周运动时,对应视图会以相同的方向和速度旋转。UILongPressGestureRecognizer:长按手势识别器,想要成功触发长按手势,需使用一个或多个手指在视图上按压不小于minimumPressDuration设定的时长,默认0.5秒,并且长按时手指移动的距离要小于allowableMovement设定的距离,默认距离10points。UIScreenEdgePanGestureRecognizer:屏幕边缘轻扫手势识别器,继承自UIPanGestureRecognizer,前面六个手势识别器均继承自UIGestureRecognizer。屏幕边缘轻扫手势识别器只能识别在屏幕边缘附近开始的手势。与滑动手势很像,不同之处在于前者必须从屏幕边缘开始。可以使用屏幕边缘手势来启动视图控制器转换。
整体而言,手势分为两种,离散或者连续
离散的手势识别器从手势开始到结束只向目标发送单个动作消息,例如点击手势、滑动手势。连续手势的手势识别器为每个增量变化发送动作消息,直到多点触摸序列结束,例如平移手势、捏合手势、旋转手势
一般来说,手势识别器调用响应方法时,会把手势识别器自己作为参数,以便在处理事件时使用额外信息
swift
- (void)handleTapGesture;
- (void)handleTapGestureWithGestureRecognizer:(UITapGestureRecognizer *)gestureRecognizer;
当然这样的写法并不是必须的,但是如果你需要获取手势在视图控制器中的位置,可以通过参数gestureRecognizer 来调用locationInView 方法来获取
一个手势识别器只能添加到一个视图,一个视图上可以添加多个手势识别器
for example:
一个照片视图可添加捏合和旋转手势,这样就可以实现放大缩小或者旋转图片
但是手势识别器一次只能识别一个手势,它不能把你的手势同时识别为捏合和旋转手势
实践
本次使用简单的tabbar来分成不同的页面来进行讲解,并且只涉及到前六个手势识别器
点击手势识别器
objectivec
// 创建点击手势识别器
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
tapGesture.numberOfTapsRequired = 1;
[self.targetView addGestureRecognizer:tapGesture];
可以看出来其实基础的手势识别器是很简单的
我们只需要创建,指定目标,指定响应方法,加入到你想指定的视图里即可
numberOfTapsRequired 指定了点击手势识别器触发时需要的点击次数
也就是如果想实现双击,在这里将次数改为2即可
此外手势识别器还有一个属性:
numberOfTouchesRequired 控制了手势需要的指头数量,即需要几根指头按下点击
可能在某些特定的地方可以用到

滑动手势识别器
滑动手势识别器叫做
UILongPressGestureRecognizer ,它的创建和使用基本上是相似的
UILongPressGestureRecognizer类包含以下四个属性:
minimumPressDuration:触发长按手势所需按压的最短时间,单位是秒,默认为0.5秒numberOfTouchesRequired:触发长按手势所需手指数,默认为1numberOfTapsRequired:触发长按手势所需点击数,默认为0allowableMovement:手指按压住视图后允许手指移动的最大距离,单位point,默认10 points
但在长按中,很明显是有一个过程的,即按下出触发手势识别器和抬起两个过程
为了识别当前属于什么阶段,UILongPressGestureRecognizer还有.state来帮助你判断当前状态
我们结合一下UIImpactFeedbackGenerator就可以轻松做到当在识别时触发震动这个反馈
这里给出响应方法的代码,创建和配置代码就不展示了
objectivec
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
// 长按开始
self.infoLabel.text = @"长按中...";
self.targetView.backgroundColor = [UIColor systemRedColor];
// 添加震动反馈
UIImpactFeedbackGenerator *feedback = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium];
[feedback impactOccurred];
// 旋转动画
[UIView animateWithDuration:0.3 animations:^{
self.targetView.transform = CGAffineTransformMakeRotation(M_PI);
}];
} else if (gesture.state == UIGestureRecognizerStateEnded) {
// 长按结束
self.infoLabel.text = @"长按完成";
self.targetView.backgroundColor = [UIColor systemGreenColor];
// 恢复旋转
[UIView animateWithDuration:0.3 animations:^{
self.targetView.transform = CGAffineTransformIdentity;
}];
}
}

滑动手势识别器
滑动手势可以滑向上下左右的任一方向,但不包括对角线方向
使用UISwipeGestureRecognizerDirection类提供的dircetion属性(枚举值)指定手势的滑动方向
如果没有设置滑动方向,默认向右侧滑动
每一个手势识别器只能识别一个滑动方向,所以如果需要识别多个方向,需要添加多个手势识别器
另外,滑动手势识别器的动作方法在滑动手势结束那一刻被调用
这里直接给出配置方法
objectivec
// 创建各个方向的滑动手势识别器
UISwipeGestureRecognizer *swipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
swipeUp.direction = UISwipeGestureRecognizerDirectionUp;
[self.targetView addGestureRecognizer:swipeUp];
UISwipeGestureRecognizer *swipeDown = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
swipeDown.direction = UISwipeGestureRecognizerDirectionDown;
[self.targetView addGestureRecognizer:swipeDown];
UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
swipeLeft.direction = UISwipeGestureRecognizerDirectionLeft;
[self.targetView addGestureRecognizer:swipeLeft];
UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
swipeRight.direction = UISwipeGestureRecognizerDirectionRight;
[self.targetView addGestureRecognizer:swipeRight];
对应的,由于我们这里加入了四个手势识别器,我们也得在触发方法里去识别这四个手势识别器,并给出相应的反馈,这里我还同时创建了一个变化矩阵来执行对应的动画
objectivec
switch (gesture.direction) {
case UISwipeGestureRecognizerDirectionUp:
direction = @"向上";
translation = CGPointMake(0, -50);
break;
case UISwipeGestureRecognizerDirectionDown:
direction = @"向下";
translation = CGPointMake(0, 50);
break;
case UISwipeGestureRecognizerDirectionLeft:
direction = @"向左";
translation = CGPointMake(-50, 0);
break;
case UISwipeGestureRecognizerDirectionRight:
direction = @"向右";
translation = CGPointMake(50, 0);
break;
default:
break;
}
这样,这个页面就可以被四个不同方向的滑动所影响

平移手势识别器
平移手势识别器(UIPanGestureRecognizer),或称为拖动手势识别器
这个手势可以使用户按压住视图在窗口内随意拖动
在用户拖动的过程中,必须保持手指一直按压在视图上。使用UIPanGestureRecognizer类的velocityInView: 方法可以获取拖动手势移动的速度
objectivec
// 创建拖动手势识别器
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[self.targetView addGestureRecognizer:panGesture];
...
// 响应方法
- (void)handlePan:(UIPanGestureRecognizer *)gesture {
CGPoint touchPoint = [gesture locationInView:self.targetView];
self.targetView.center = touchPoint;
}
旋转和捏合手势
旋转和捏合手势我放在了一块来讲解
捏合手势识别器(UIPinchGestureRecognizer)当用户两个手指靠近时缩小视图,两个手指远离时放大视图
最常见的就是浏览照片时对照片捏合缩放,但在这一部分没有使用UIImageView,而是在UIView上添加UIPinchGestureRecognizer
捏合手势识别器必须两个手指同时操作,这一点与前面几个示例不同
旋转手势识别器(UIRotationGestureRecognizer)与捏合手势类似,都需要两个手指,通过修改rotation来改变视图的变换。通常,旋转手势与其他手势组合使用
objectivec
// 创建捏合手势识别器
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
[self.targetView addGestureRecognizer:pinchGesture];
...
// 创建旋转手势识别器
UIRotationGestureRecognizer *rotationGesture = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotation:)];
[self.targetView addGestureRecognizer:rotationGesture];
先来讲解捏合操作
捏合操作可能看起来非常简单,但在实际处理中,我们可能会遇到相当复杂的问题,例如视图不断疯狂放大(没有将属性重置),放大效果不跟手等
想要优秀的处理捏合操作的反馈部分,需要属性Core Animation框架内容中的返射矩阵变换的内容,可以参考我之前的文章:
先给出我的响应方法:
objectivec
UIPinchGestureRecognizer 内部会计算比例:D2/D1=1.1(假设放大了 10%)- (void)handlePinch:(UIPinchGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
// 捏合开始
self.infoLabel.text = @"捏合中...";
} else if (gesture.state == UIGestureRecognizerStateChanged) {
// 捏合中
self.currentScale *= gesture.scale;
// 限制缩放范围
self.currentScale = MAX(0.5, MIN(3.0, self.currentScale));
self.targetView.transform = CGAffineTransformMakeScale(self.currentScale, self.currentScale);
// 更新标签显示缩放比例
self.infoLabel.text = [NSString stringWithFormat:@"缩放比例: %.2fx", self.currentScale];
gesture.scale = 1.0;
} else if (gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) {
// 捏合结束
self.infoLabel.text = @"捏合完成";
}
}
重点来看当state处于UIGestureRecognizerStateChanged 的时候
我们先来假定一个场景,我的视图为100*100规模,我们的两个指头做放大的动作
当你的两根手指放上去的时候,距离是D1,然后张开一点,距离变为了D2
此时,UIPinchGestureRecognizer 内部会计算比例:D2/D1=1.1(假设放大了 10%),pinchGestureRecognizer.scale 的值变成了 1.1,系统触发方法
再来看我们的方法,在方法内,我们先存储了scale 的数值,便于我们来进行范围限制(即最大不超过三倍,最小值不小于一半
然后我们创建了一个变化矩阵,变化矩阵会将控件的长和宽缩放到我们指定的倍率,即1.1倍
最后我们将手势识别器的scale 重新置为1
注意!!!!!
注意我对缩放倍率的记录,
objectivec
self.currentScale *= gesture.scale;
为了限制缩放倍率,同时不影响视图,我把scale拿出来记录过,并且为自乘操作
你知道吗,如果不设置为1,或者不记录,你的控件缩放会极度炸裂
我们来看一下假如不使用本地记录当前缩放倍率会怎么样(这里一定要去看transfrom的属性特性)
在我们第一次设置缩放矩阵之后,视图假如说被设定为1.1倍
之后我们把手抬起,重新缩放
此时手势识别器的scale 会被置为1,然后重新缩放(比如1.5)
你的视图的变化矩阵会从1.1变化为1.5倍,即1.0*1.5(因为之前的变化矩阵会被覆盖)
但是我们在缩放视图的时候如果连续两次缩放,我们更加希望视图尺寸可以变为1.1*1.5
这就是为什么我们要本地记录当前的缩放倍率
最后我们来讲讲为什么需要将手势识别器置1
手势识别器,在内部识别数据变化后,会再次出发方法,所以如果布置为1,在浅浅暂停过后的放大时,你的倍率会每秒都乘那个数字

接下来讲一下旋转手势视图控制器
在旋转手势视图控制器的响应中,其实处理方法是差不多的,所以这里不在赘述,直接给出方法和效果
objectivec
- (void)handleRotation:(UIRotationGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
// 旋转开始
self.infoLabel.text = @"旋转中...";
} else if (gesture.state == UIGestureRecognizerStateChanged) {
// 旋转中
self.currentRotation += gesture.rotation;
self.targetView.transform = CGAffineTransformMakeRotation(self.currentRotation);
// 计算角度(转换为度数)
CGFloat angleInDegrees = self.currentRotation * 180.0 / M_PI;
self.infoLabel.text = [NSString stringWithFormat:@"旋转角度: %.1f°", angleInDegrees];
gesture.rotation = 0;
} else if (gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) {
// 旋转结束
self.infoLabel.text = @"旋转完成";
}
}

需要注意这两个手势识别器都需要本地存储当前变化,再在当前变化中叠加变化,构造变化矩阵
重点就是如何处理本地的状态记录以及手势识别器的状态叠加
参考资料: