触摸事件UITouch

来源:互联网 发布:网络对讲机软件 编辑:程序博客网 时间:2024/06/05 04:22


1.详情介绍


#import<Foundation/Foundation.h>

#import<CoreGraphics/CoreGraphics.h>

#import<UIKit/UIKitDefines.h>


@classUIWindow,UIView,UIGestureRecognizer;


typedefNS_ENUM(NSInteger, UITouchPhase) {

    UITouchPhaseBegan,            //开始触摸

    UITouchPhaseMoved,            //触摸移动中

    UITouchPhaseStationary,       //触摸中但是没有移动

    UITouchPhaseEnded,            //触摸结束

    UITouchPhaseCancelled,        // 触摸被中断(系统的,如电话)

};

typedefNS_ENUM(NSInteger, UIForceTouchCapability) {

    UIForceTouchCapabilityUnknown =0,

    UIForceTouchCapabilityUnavailable =1,

    UIForceTouchCapabilityAvailable =2

};

typedefNS_ENUM(NSInteger, UITouchType) {

    UITouchTypeDirect,                      // 直接触摸

    UITouchTypeIndirect,                    // 间接触摸

    UITouchTypeStylusNS_AVAILABLE_IOS(9_1),// 笔 触摸

}NS_ENUM_AVAILABLE_IOS(9_0);


typedefNS_OPTIONS(NSInteger, UITouchProperties) {

    UITouchPropertyForce = (1UL <<0),//力度

    UITouchPropertyAzimuth = (1UL <<1),//方位

    UITouchPropertyAltitude = (1UL <<2),//高度

    UITouchPropertyLocation = (1UL <<3),// 位置

}NS_AVAILABLE_IOS(9_1);


NS_CLASS_AVAILABLE_IOS(2_0)@interface UITouch :NSObject


@property(nonatomic,readonly)NSTimeInterval      timestamp;//时间戳

@property(nonatomic,readonly)UITouchPhase        phase;//触摸的状态

@property(nonatomic,readonly)NSUInteger          tapCount;  // 触摸的手指数

@property(nonatomic,readonly)UITouchType         typeNS_AVAILABLE_IOS(9_0);//触摸的类型


@property(nonatomic,readonly)CGFloat majorRadiusNS_AVAILABLE_IOS(8_0);//触摸的半径

@property(nonatomic,readonly)CGFloat majorRadiusToleranceNS_AVAILABLE_IOS(8_0);


@property(nullable,nonatomic,readonly,strong)UIWindow                        *window;

@property(nullable,nonatomic,readonly,strong)UIView                          *view;

@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizersNS_AVAILABLE_IOS(3_2);


- (CGPoint)locationInView:(nullableUIView *)view;//现在触摸的坐标

- (CGPoint)previousLocationInView:(nullableUIView *)view;//上一次触摸的坐标

- (CGPoint)preciseLocationInView:(nullableUIView *)view NS_AVAILABLE_IOS(9_1);//现在触摸的精确的坐标

- (CGPoint)precisePreviousLocationInView:(nullableUIView *)view NS_AVAILABLE_IOS(9_1);//上一次触摸的精确的坐标


@property(nonatomic,readonly)CGFloat forceNS_AVAILABLE_IOS(9_0);//触摸的力度

@property(nonatomic,readonly)CGFloat maximumPossibleForceNS_AVAILABLE_IOS(9_0);//触摸的最大的力度


- (CGFloat)azimuthAngleInView:(nullableUIView *)view NS_AVAILABLE_IOS(9_1);


- (CGVector)azimuthUnitVectorInView:(nullableUIView *)view NS_AVAILABLE_IOS(9_1);


@property(nonatomic,readonly)CGFloat altitudeAngleNS_AVAILABLE_IOS(9_1);


@property(nonatomic,readonly)NSNumber * _Nullable estimationUpdateIndexNS_AVAILABLE_IOS(9_1);


@property(nonatomic,readonly)UITouchProperties estimatedPropertiesNS_AVAILABLE_IOS(9_1);


@property(nonatomic,readonly)UITouchProperties estimatedPropertiesExpectingUpdatesNS_AVAILABLE_IOS(9_1);


@end


2.关于响应者的概念,通过以下几点说明:

响应者对象(Response object

响应者对象就是可以响应事件并对事件作出处理。在iOS中,存在UIResponder类,它定义了响应者对象的所有方法。UIApplication、UIView等类都继承了UIResponder类,UIWindow和UIKit中的控件因为继承了UIView,所以也间接继承了UIResponder类,这些类的实例都可以当作响应者。

    第一响应者(First responder):

       当前接受触摸的响应者对象被称为第一响应者,即表示当前该对象正在与用户交互,它是响应者链的开端。
 响应者链(Responder chain):

      响应者链表示一系列的响应者对象。事件被交由第一响应者对象处理,如果第一响应者不处理,事件被沿着响应者链向上传递,交给下一个响应者(next responder)。一般来说,第一响应者是个视图对象或者其子类对象,当其被触摸后事件被交由它处理,如果它不处理,事件就会被传递给它的视图控制器对象(如果存在),然后是它的父视图(superview)对象(如果存在),以此类推,直到顶层视图。接下来会沿着顶层视图(top view)到窗口(UIWindow对象)再到程序(UIApplication对象)。如果整个过程都没有响应这个事件,该事件就被丢弃。一般情况下,在响应者链中只要由对象处理事件,事件就停止传递。但有时候可以在视图的响应方法中根据一些条件判断来决定是否需要继续传递事件。


管理事件分发

视图对触摸事件是否需要作处回应可以通过设置视图的userInteractionEnabled属性。默认状态为YES,如果设置为NO,可以阻止视图接收和分发触摸事件。除此之外,当视图被隐藏(setHidden:YES)或者透明(alpha值为0)也不会收事件。不过这个属性只对视图有效,如果想要整个程序都步响应事件,可以调用UIApplication的beginIngnoringInteractionEvents方法来完全停止事件接收和分发。通过endIngnoringInteractionEvents方法来恢复让程序接收和分发事件。如果要让视图接收多点触摸,需要设置它的multipleTouchEnabled属性为YES,默认状态下这个属性值为NO,即视图默认不接收多点触摸。


处理用户的触摸事件

首先触摸的对象是视图,而视图的类UIView继承了UIRespnder类,但是要对事件作出处理,还需要重写UIResponder类中定义的事件处理函数。根据不通的触摸状态,程序会调用相应的处理函数,这些函数包括以下几个:

            -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;

            -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;

            -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;

            -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

            当手指接触屏幕时,就会调用touchesBegan:withEvent方法;

            当手指在屏幕上移时,动就会调用touchesMoved:withEvent方法;

            当手指离开屏幕时,就会调用touchesEnded:withEvent方法;

            当触摸被取消(比如触摸过程中被来电打断),就会调用touchesCancelled:withEvent方法。而这几个方法被调用时,正好对应了UITouch类中phase属性的4个枚举值。

            上面的四个事件方法,在开发过程中并不要求全部实现,可以根据需要重写特定的方法。对于这4个方法,都有两个相同的参数:NSSet类型的touches和UIEvent类型的event。其中touches表示触摸产生的所有UITouch对象,而event表示特定的事件。因为UIEvent包含了整个触摸过程中所有的触摸对象,因此可以调用allTouches方法获取该事件内所有的触摸对象,也可以调用touchesForVIew:或者touchesForWindows:取出特定视图或者窗口上的触摸对象。在这几个事件中,都可以拿到触摸对象,然后根据其位置,状态,时间属性做逻辑处理。


3.项目实践


下面的代码示例了对消息处理方法的声明让进行双击操作时增加所点击视图的大小而单击操作时减少所点击视图的大小。

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {

UITouch*touch = [touches anyObject];if(2== touch.tapCount) {

[NSObjectcancelPreviousPerformRequestsWithTarget:self];}

}

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { }

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {UITouch*touch = [touchesanyObject];
if (1== touch.tapCount) {

NSDictionary*touchLoc = [NSDictionarydictionaryWithObject:
[NSValue valueWithCGPoint:[touchlocationInView:self]]

forKey:@"location"];
[self performSelector:@selector(handleSingleTap:)withObject:touchLoc

afterDelay:0.3];}

else if(2== touch.tapCount) {
//double-tap: increase view size by 10%
CGRect viewFrame = self.frame;viewFrame.size.width+=self.frame.size.width*0.1;viewFrame.size.height+=self.frame.size.height*0.1;viewFrame.origin.x-= (self.frame.origin.x*0.1)/2.0;viewFrame.origin.y-= (self.frame.origin.y*0.1)/2.0;

[UIViewbeginAnimations:nilcontext:NULL];[selfsetFrame:viewFrame];
[UIView commitAnimations];

}}

- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { }

- (void)handleSingleTap:(NSDictionary*)touches {//Single-tap: decrease view size by 10%
CGRect viewFrame = self.frame;viewFrame.size.width-=self.frame.size.width*0.1;

viewFrame.size.height-=self.frame.size.height*0.1;viewFrame.origin.x+= (self.frame.origin.x*0.1)/2.0;viewFrame.origin.y+= (self.frame.origin.y*0.1)/2.0;

[UIViewbeginAnimations:nilcontext:NULL];[selfsetFrame:viewFrame];
[UIView commitAnimations];

}
以下是对上面代码的说明:

在touchEnded:withEvent:方法中,当点击的次数为1时,响应者对象向其自身发送performSelector:withObject:afterDelay:消息。选择子标识了对单击事件响应的方法;第二个参数是一个NSValue或NSDictionary对象,用来保存UITouch对象的一些状态;延迟时间是表示单击和双击之间有意义的时间间隔。

注:因为touch对象在触摸序列的处理中是可变的,所以你不能retain一个touch对象并假设它的状态不会发生改变(你也不能拷贝一个touch对象,因为其没有申明NSCopying协议)。但如果你需要保存touch对象的某些状态,你需要将touch对象的状态用NSVaule对象,字典对象或其它类似对象进行保存。

在touchBegan:withEvent:方法中,当点击的次数为2时,响应者对象通过调用NSObject类的cancelPerformRequestsWithTarget:方法取消被挂起的延迟调用。如果点击次数不为2,则在延迟时间到达后,将会调用选择子所指定的单击事件处理方法。

在touchEnded:withEvent:方法中,当点击的次数为2时,响应者对双击的操作进行处理。

处理滑动或拖动手势

水平或垂直方面的滑动属于简单的手势,你可以在你的代码中进行跟踪并对其进行处理。要检测一个滑动手势,你必须跟踪与所希望的移动轴相反的用户手指的移动,但是滑动的组成最终还是由你来决定。换句话说,你需要确定用户手指是否移动的足够远,是否以直线方式向前移动,是否移动的足够快。要对上述的内容进行判断,你要存储下初始时的触摸位置,然后与touch-moved事件中位置进行比较。

下面的代码示例了一些基本的跟踪方法用于判断视图上进行水平方向的滑动。在这个例子中首先将触摸的初始位置存储到startTouchPosition实例变量中。当用户移动手指,代码对当前的触摸位置与初始触摸位置进行比较来确定是否为滑动操作。如果手指在垂直方向上移动了太多的距离,则不认为是一次滑动而进行不同的处理。一旦滑动足够远并被认为是一个完整的手势时会调用相应的操作。要在垂直方向上判断滑动操作,你也可以使用类似的代码,只不过对对X和Y方向进行交换。

#define HORIZ_SWIPE_DRAG_MIN12#define VERT_SWIPE_DRAG_MIN4

#pragma mark event handle

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {UITouch*touch = [touchesanyObject];

startTouchPosition= [touch locationInView:self];

}

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {UITouch*touch = [touchesanyObject];
CGPoint currentTouchPosition = [touch locationInView:self];

if((fabs(startTouchPosition.x- currentTouchPosition.x) >=HORIZ_SWIPE_DRAG_MIN) &&

(fabs(startTouchPosition.y- currentTouchPosition.y) <=VERT_SWIPE_DRAG_MIN)) {

if(startTouchPosition.x< currentTouchPosition.x) {NSLog(@"swipe to right");

}else{

NSLog(@"swipe to left");}

}}

  • -  (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {startTouchPosition= CGPointZero;

    }

  • -  (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {startTouchPosition= CGPointZero;

    }
    下面的代码是一个更简单的单触摸事件的跟踪,这次则是用来对拖动进行处理。在本次

    处理中只有touchesMoved:withEvent:方法有实现代码,在实现代码中对本次位置和上次位置取差值。然后使用差值来重新设置视图的位置。
    - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    }

    - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {UITouch*touch = [touchesanyObject];
    CGPoint currentTouchPosition = [touch locationInView:self];CGPointprevTouchPosition = [touchpreviousLocationInView:self];

    CGRectviewFrame = self.frame;
    float deltaX = currentTouchPosition.x- prevTouchPosition.x;floatdeltaY = currentTouchPosition.y- prevTouchPosition.y;viewFrame.origin.x+= deltaX;
    viewFrame.origin.y+= deltaY;

    [selfsetFrame:viewFrame];}

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {}

- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {}

处理复杂的多点触摸序列

  点击,拖动和滑动只是简单的手势,通常只涉及一次触摸。要对两个或两个以上的触摸事件进行处理则更为复杂。你可能要跟踪在整个触摸阶段的所有触摸,记录被更改的触摸属性以及更改内部状态为合适值。为了能对多触摸事件进行跟踪和处理还需要做如下的事情:

· 设置视图的multipleTouchEnabled属性为YES

· 使用CoreFoundation的字典对象(CFDictionaryRef)在事件过程的各阶段中跟踪触摸的改变

当处理一个多触摸事件时,你经常会存储每个触摸的最初状态以用于以后的比较。作为例子,你将对每个触摸的最后位置与初始位置进行比较。在touchesBegan:withEvent:方法中,你将从locationInView:方法加获得每个触摸的初始位置并将这些位置以UITouch对象的地址作为键值存储到CFDictionaryRef对象中。在touchesEnded:withEvent:方法中,你将从字典对象中取出初始位置值并与当前位置值进行比较(你应该使用CFDictionaryRef对象而非NSDictionary对象,后一个对象对键值进行拷贝操作,但UITouch对象并没有申明用于对象拷贝的NSCopying协议)。

下面的代码示例了使用Core Foundation字典对象对初始位置进行存储。

- (void)cacheBeginPointForTouches: (NSSet*)touches {if([touches count] > 0) {

for(UITouch*touch in touches) {
CGPoint *point = (CGPoint *)CFDictionaryGetValue(touchBeginPoints,

touch);
if (NULL== point) {

point = (CGPoint*)malloc(sizeof(CGPoint));

CFDictionarySetValue(touchBeginPoints, touch, point);}

*point = [touch locationInView:self.superview];}

}}

- (void)comparePointForTouches: (NSSet*)touches {NSArray*sortedTouch = [[touchesallObjects]

sortedArrayUsingSelector:@selector(compareAddress:)];UITouch*touch1 = [sortedTouchobjectAtIndex:0];

下面的代码示例了如何从字典对象中取得初始位置数据并获得对应触摸的当前位置。

UITouch*touch2 = [sortedTouch objectAtIndex:1];

CGPointbeginPoint1 = *((CGPoint*)CFDictionaryGetValue(touchBeginPoints,touch1));

CGPointcurrentPoint1 = [touch1 locationInView:self.superview];

CGPointbeginPoint2 = *((CGPoint*)CFDictionaryGetValue(touchBeginPoints,touch2));

CGPointcurrentPoint2 = [touch2 locationInView:self.superview];//Compare current and begin point

}
更具体的操作可参考apple提供的MoveMe示例。

Hit-Testing

你自己定义的响应者可以使用hit-testing来找到要对与触摸对应的该响应者中的子视图或子层(subLayer)并对其进行合适的处理。要完成该操作可以调用视图的hitTest:withEvent:方法或子层的hitTest:方法或者直接重载其中任一方法。响应者有时会在事件转发之前对hitTest进行处理。

如果传入hitTest:withEvent:或hitTest:方法的触摸点位置在包括此方法的视图范围之内,则会被忽略。也就是说子视图在其父视图的范围之外则不会收到触摸事件。

如果你自定的视图有子视图,你必须决定是否在子视图层或父视图层对触摸进行处理。如果子视图没有声明touchesBegan:withEvent:,touchesMoved:withEvent:或touchesEnded:withEvent:方法对触摸进行处理,则相关的事件会传递给它的父视图。对于多次点击或多点触摸都是与首个触摸子视图相联系,所以父视图是不会收到触摸消息的。要确认能收到所有的触摸消息,父视图要重载hitTest:withEvent:方法并返回其自身对象而不是其子视图。

下面的代码示例用于检测位于自定义视图层上的“info”图片何时被点击

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
CGPoint location = [[touches anyObject] locationInView:self];
CALayer *hitLayer = [[selflayer]hitTest:[selfconvertPoint:locationfromView:nil]];

if(hitLayer) {
[self displayInfo];

}}

下面的代码,一个响应者类(在本例中为UIWindow的子类)重载了hitTest:withEvent:方法。首先返回父类的hit-test视图,如果该视图是其本身,则返回其最后一个子视图。

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {UIView*hitView = [superhitTest:pointwithEvent:event];

if(hitView == self) {
return [[selfsubviews]lastObject];

}

else{
return hitView;

}}

转发触摸事件

事件转发是用于一些应用程序的一种技术。你通过调用另一个响应者对象的事件处理方法将触摸事件进行转发。尽管这是个很有效率的方法,但在使用过程中还有很多需要注意的地方。UIKitFramework中的类并不是设计用于接收不在其范围内的触摸事件,从编程的角度来看,也就是UITouch对象的view属性必须持有一个对framework对象的引用以用来对触摸事件进行处理。如果你想在你的应用程序中有选择的将触摸事件传递给其它响应者,用于接收的响应者必须是你自己定义的UIView的子类。

例如:一个应用程序有三个自定义的子视图:A,B和C。当用户点击了视图A,应用程序的窗口对象确定所点击的视图是个hit-test视图并向其发送初始触摸事件。根据一定的条件,视图A将事件转发给视图B或C。在这种情况下,视图A,B和C必须注意要使事件能进行转发,视图B和C要能对不在其范围内的触摸进行处理。

  事件转发经常需要分析触摸对象用来确定向何处转发事件。有些方法可以用来进行这样的分析:

对于一个视图,使用hit-testing对事件进行分析在将其转发到子视图之前

在UIWindow的自定义子类中重载sendEvent:方法,对触摸进行分析并将其转发给合适的响应者。如果重载了该方法,你就必须调用父类的sendEvent:方法

  设计你的应用其中并不需要对触摸进行分析

下面的代码示例了第二种方法,自定义一个UIWindow的子类并对sendEvent:方法进行重载。本例中,子类对象将触摸事件转发给一个自定义的”helper”响应者,在该响应者中对相应的视图进行affine变换。

- (void)sendEvent:(UIEvent*)event{

for(TransformGesture *gesture in transformGestures) {
// collect all the touches we care about from the eventNSSet*touches = [gesture observedTouchesForEvent:event];NSMutableSet*began =nil;
NSMutableSet *moved = nil;
NSMutableSet *ended = nil;
NSMutableSet *cancelled = nil;

// sort the touches by phase so we can handle them similarly to normal eventdispatch

for(UITouch*touchin touches) {switch([touchphase]) {

caseUITouchPhaseBegan:if(!began)

began = [NSMutableSetset];[beganaddObject:touch];break;

caseUITouchPhaseMoved:if(!moved)

moved = [NSMutableSetset];[movedaddObject:touch];break;

caseUITouchPhaseEnded:if(!ended)

ended = [NSMutableSetset];[endedaddObject:touch];break;

caseUITouchPhaseCancelled:if(!cancelled)

cancelled = [NSMutableSetset];[cancelledaddObject:touch];break;

default:break;

}}

// call our methods to handle the touches

if(began)
[gesture touchesBegan:began withEvent:event];

if(moved)
[gesture touchesMoved:moved withEvent:event];

if(ended)
[gesture touchesEnded:ended withEvent:event];

if(cancelled)
[gesture touchesCancelled:cancelled withEvent:event];

}

[supersendEvent:event];} 






0 0
原创粉丝点击