UITouch - BNR
本节任务:创建一个视图,让用户在视图上拖动手指来画线。
UIView类能够重载4个方法来处理不同的触摸事件。
- (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 //系统事件,在触摸结束前被其打断。
@property(nonatomic, getter=isMultipleTouchEnabled) BOOL multipleTouchEnabled // 默认为NO,如果想要处理多手指触摸事件,则需设置为YES。
一个UITouch对应在屏幕上的一个手指。如果一个视图开始拥有触摸对象,该视图将拥有触摸对象的生命周期。你不应该引用触摸对象。
创建一个新的Single View Application工程,命名为TouchTracker。
首先,需要一个模型对象来描述line。创建一个NSObject的子类,取名为BNRLine。
再创建一个UIView的子类,取名为BNRDrawView。BNRDrawView用来追踪全部画好了的线条以及目前正在画的线条。
再创建一个UIViewController的子类来管理BNRDrawView对象,取名为BNRDrawViewController。
在BNRLine.h头文件中,声明两个CGPoint属性,如下:
1 @property (nonatomic) CGPoint begin; 2 @property (nonatomic) CGPoint end;
同时,将 #import <Foundation/Foundation.h> 改为 #import <UIKit/UIKit.h> 。
打开BNRDrawViewController.m文件,重载loadView方法,将BNRDrawView对象设置为BNRDrawViewController的view。
当请求显示控制器的view时,但是为nil,则视图控制器会自动调用loadView方法。该方法应加载或创建一个视图,并将其赋给控制器的view属性。如果视图控制器有一个相对于的nib文件,loadView方法将从该nib文件加载view。如果你使用了Interface Builder创建了视图并初始化了视图控制器,你一定不能重载该方法。
如果使用Interface Builder设计用户界面时,当视图对象从nib文件中加载视图时,initWithFrame:方法将不会被调用。
1 - (void)loadView { 2 self.view = [[BNRDrawView alloc] initWithFrame:CGRectZero]; 3 }
同时,在实现文件顶部导入BNRDrawView头文件,如下:
#import "BNRDrawView.h"
打开AppDelegate.m文件,创建一个BNRDrawViewController对象,将其作为window的rootViewController。同时导入BNRDrawViewController的头文件, #import "BNRDrawViewController.h" 。
方法application:didFinishLaunchingWithOptions:方法修改如下:
1 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 2 // Override point for customization after application launch. 3 self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 4 BNRDrawViewController *dvc = [[BNRDrawViewController alloc] init]; 5 self.window.rootViewController = dvc; 6 7 self.window.backgroundColor = [UIColor yellowColor]; 8 [self.window makeKeyAndVisible]; 9 return YES; 10 }
打开BNRDrawView.m,导入BNRLine头文件,并在类扩展中声明两个属性,如下:
1 #import "BNRLine.h" 2 3 @interface BNRDrawView () 4 5 @property (nonatomic, strong) BNRLine *currentLine; 6 @property (nonatomic, strong) NSMutableArray *finishedLines; 7 8 @end
同时,添加initWithFrame:代码如下:
1 - (instancetype)initWithFrame:(CGRect)r { 2 self = [super initWithFrame:r]; 3 if (self) { 4 self.finishedLines = [[NSMutableArray alloc] init]; 5 self.backgroundColor = [UIColor grayColor]; 6 } 7 return self; 8 }
同时,需要实现对finishedLine和currentLine的绘制。
当一个视图第一次显示或者一个事件的发生使得视图可见的部分失效了,则drawRect:方法会被调用。绝对不能直接调用该方法。为了使失效的部分重新绘制,需调用setNeedsDisplay或setNeedsDisplayInRect:方法。如下:
1 - (void)strokeLike:(BNRLine *)line { 2 UIBezierPath *bp = [UIBezierPath bezierPath]; 3 bp.lineWidth = 10; 4 bp.lineCapStyle = kCGLineCapRound; 5 6 [bp moveToPoint:line.begin]; 7 [bp addLineToPoint:line.end]; 8 [bp stroke]; 9 } 10 11 - (void)drawRect:(CGRect)rect { 12 [[UIColor blackColor] set]; 13 for (BNRLine *line in self.finishedLines) { 14 [self strokeLike:line]; 15 } 16 if (self.currentLine) { 17 [[UIColor redColor] set]; 18 [self strokeLike:self.currentLine]; 19 } 20 }
该工程的对象关系如下所示:
当触摸事件开始时,需要创建一个line,将begin和end设置为触摸开始的点位置处。当手指移动时,将更新end。因此,打开BNRDrawView.m文件,touchesBegin:withEvent:方法实现如下:
1 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 2 UITouch *t = [touches anyObject]; 3 CGPoint location = [t locationInView:self]; 4 self.currentLine = [[BNRLine alloc] init]; 5 self.currentLine.begin = location; 6 self.currentLine.end = location; 7 [self setNeedsDisplay]; 8 }
touchesMoved:withEvent:方法实现如下:
1 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 2 UITouch *t = [touches anyObject]; 3 CGPoint location = [t locationInView:self]; 4 self.currentLine.end = location; 5 [self setNeedsDisplay]; 6 }
当触摸事件结束时,将currentLine添加到finishedLines,touchesEnded:withEvent:实现如下:
1 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 2 [self.finishedLines addObject:self.currentLine]; 3 self.currentLine = nil; 4 [self setNeedsDisplay]; 5 }
启动程序,当正在画线时,线条显示红色。当画线结束,将变为黑色。
默认情况下,一个视图将一次只接受一个触摸。
此时,需要一个属性来包含尽可能多的将在屏幕上画出的线条。打开BNRDrawView.m,用下列的属性代替currentLine。
@property (nonatomic, strong) NSMutableDictionary *linesInProgress;
修改BNRDrawView中initWithFrame:方法使其能接收多点触摸,并初始化linesInProgress属性,如下:
1 - (instancetype)initWithFrame:(CGRect)r { 2 self = [super initWithFrame:r]; 3 if (self) { 4 self.linesInProgress = [[NSMutableDictionary alloc] init]; 5 self.finishedLines = [[NSMutableArray alloc] init]; 6 self.backgroundColor = [UIColor grayColor]; 7 self.multipleTouchEnabled = YES; 8 } 9 return self; 10 }
修改触摸响应事件如下:
1 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 2 NSLog(@"%@", NSStringFromSelector(_cmd)); 3 4 for (UITouch *t in touches) { 5 CGPoint location = [t locationInView:self]; 6 BNRLine *line = [[BNRLine alloc] init]; 7 line.begin = location; 8 line.end = location; 9 NSValue *key = [NSValue valueWithNonretainedObject:t]; 10 self.linesInProgress[key] = line; 11 } 12 [self setNeedsDisplay]; 13 } 14 15 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 16 NSLog(@"%@", NSStringFromSelector(_cmd)); 17 18 for (UITouch *t in touches) { 19 NSValue *key = [NSValue valueWithNonretainedObject:t]; 20 BNRLine *line = self.linesInProgress[key]; 21 line.end = [t locationInView:self]; 22 } 23 24 [self setNeedsDisplay]; 25 } 26 27 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 28 NSLog(@"%@", NSStringFromSelector(_cmd)); 29 30 for (UITouch *t in touches) { 31 NSValue *key = [NSValue valueWithNonretainedObject:t]; 32 BNRLine *line = self.linesInProgress[key]; 33 [self.finishedLines addObject:line]; 34 [self.linesInProgress removeObjectForKey:key]; 35 } 36 [self setNeedsDisplay]; 37 }
其中,我们使用valueWithNonretainedObject:方法来产生key用于保存BNRLine。该方法产生的NSValue对象将拥有与该lian有关的UITouch对象的地址。在每次触摸事件中。UITouch对象的地址是保持不变的。
在一个NSDictionary中,object使用的key必须遵守NSCopying协议,该协议允许他们接收copy方法。
最后,修改drawRect:方法:
1 - (void)drawRect:(CGRect)rect { 2 [[UIColor blackColor] set]; 3 for (BNRLine *line in self.finishedLines) { 4 [self strokeLike:line]; 5 } 6 [[UIColor redColor] set]; 7 for (NSValue *key in self.linesInProgress) { 8 [self strokeLike:self.linesInProgress[key]]; 9 } 10 }
最后,添加touchesCancelled:withEvent:方法。当一个触摸事件取消时,应将在linesInProgress中的线条全部移除。如下:
1 - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 2 NSLog(@"%@", NSStringFromSelector(_cmd)); 3 for (UITouch *t in touches) { 4 NSValue *key = [NSValue valueWithNonretainedObject:t]; 5 [self.linesInProgress removeObjectForKey:key]; 6 } 7 [self setNeedsDisplay]; 8 }