2015/11/26
Day 41
今天开始学起触摸事件
在用户使用app过程中,会产生各种各样的事件
iOS中的事件可以分为3大类型
响应者对象
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”
UIApplication、UIViewController、UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件
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;
- 加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- 远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
- 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
- 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
提示:touches中存放的都是UITouch对象
- 当用户用一根触摸屏幕时,会创建一个与手指相关联的UITouch对象
- 一根手指对应一个UITouch对象
UITouch的作用
- 保存着跟手指相关的信息,比如触摸的位置、时间、阶段
- 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
- 当手指离开屏幕时,系统会销毁相应的UITouch对象
- 提示:iPhone开发中,要避免使用双击事件!
UITouch的属性
- 触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
- 触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view;
- 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
- 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
- 当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
UITouch的方法
- (CGPoint)locationInView:(UIView *)view;
返回值表示触摸在view上的位置
这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)previousLocationInView:(UIView *)view;
该方法记录了前一个触摸点的位置
UIEvent
- 每产生一个事件,就会产生一个UIEvent对象
- UIEvent:称为事件对象,记录事件产生的时刻和类型
常见属性
- 事件类型
@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;
- 事件产生的时间
@property(nonatomic,readonly) NSTimeInterval timestamp;
- UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)
- 一次完整的触摸过程,会经历3个状态:
- 触摸开始:- (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
- 4个触摸事件处理方法中,都有NSSet *touches和UIEvent *event两个参数
- 一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数
- 如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
- 如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
- 根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸
然后用上述4个方法做了一个涂鸦app
很简单,先自定义一个view
@interface YUView : UIView - (void)clear; - (void)back; @end
两个方法供控制器调用,重置和回退
@interface YUView () @property (nonatomic, strong) NSMutableArray *totalLines; @end
私有属性是个可变数组,里面装画过的所有线的路径
实现代码如下
@implementation YUView - (NSMutableArray *)totalLines { if (_totalLines == nil) { _totalLines = [NSMutableArray array]; } return _totalLines; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:touch.view]; UIBezierPath *currentPath = [UIBezierPath bezierPath]; [currentPath moveToPoint:point]; [self.totalLines addObject:currentPath]; [self setNeedsDisplay]; } - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:touch.view]; UIBezierPath *path = [self.totalLines lastObject]; [path addLineToPoint:point]; [self setNeedsDisplay]; } - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self touchesMoved:touches withEvent:event]; } - (void)drawRect:(CGRect)rect { [[UIColor blueColor] set]; for (UIBezierPath *path in self.totalLines) { path.lineWidth = 5; [path stroke]; } } - (void)clear { [self.totalLines removeAllObjects]; [self setNeedsDisplay]; } - (void)back { [self.totalLines removeLastObject]; [self setNeedsDisplay]; } @end
很简单,只要在触摸事件中存入触摸的点画线就行, 通过 [self setNeedsDisplay];重绘页面
重置只需清空路径数组,回退只需删除数组中最后一个路径即可
之前一直看swift文档,实在是太无聊,于是乎我决定把这个涂鸦用swift重构一遍,如下
class YUView : UIView { var paths:[UIBezierPath] = [] func back() { self.paths.removeLast() self.setNeedsDisplay() } func clear() { self.paths.removeAll() self.setNeedsDisplay() } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first let point = touch?.locationInView(touch?.view) let currrentPath = UIBezierPath.init() currrentPath.moveToPoint(point!) self.paths.append(currrentPath) self.setNeedsDisplay() } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first let point = touch?.locationInView(touch?.view) let path = self.paths.last path?.lineJoinStyle = .Round path?.lineCapStyle = .Round path!.addLineToPoint(point!) self.setNeedsDisplay() } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { self.touchesMoved(touches, withEvent: event) } override func drawRect(rect: CGRect) { UIColor.init(red: 0, green: 0, blue: 1, alpha: 1).set() for path in self.paths { path.lineWidth = 8 path.stroke() } } }
感觉第一次用上swift我就喜欢上它了,代码简洁而且对于我这样非常熟悉.语法的Java程序员非常亲切~
2015/11/27
Day 42
今天做了个手势解锁的页面
每个圆圈都是一个button通过改变状态改变图片,通过触摸事件画线就行了。
button最好别直接用UIButton,自己定义
@interface YUCircleView : UIButton @end @implementation YUCircleView - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self == [super initWithCoder:aDecoder]) { [self setup]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { if (self == [super initWithFrame:frame]) { [self setup]; } return self; } - (void) setup { self.userInteractionEnabled = NO; [self setImage:[UIImage imageNamed:@"gesture_node_normal"] forState:UIControlStateNormal]; [self setImage:[UIImage imageNamed:@"gesture_node_highlighted"] forState:UIControlStateSelected]; } @end
重写那两个初始化方法使按钮不管什么方式创建都设置好图片,不能互动是为了取消按钮的高亮状态
把按钮放进自定义的view里
@interface YULockView () @property (nonatomic, strong) NSMutableArray *selectedViews; @property (nonatomic, assign) CGPoint currentPoint; @end
这两个私有属性,selectedViews里面保存被选中的按钮,currentPoint保存用户当前触摸的点
先把九个按钮加上去并且排布好,注意子控件的frame要在layoutSubviews方法中执行并且要调用[super layoutSubviews];
- (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self == [super initWithCoder:aDecoder]) { [self setup]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { if (self == [super initWithFrame:frame]) { [self setup]; } return self; } - (void) setup { for (int i = 0; i < 9; i++) { YUCircleView *circleView = [YUCircleView buttonWithType:UIButtonTypeCustom]; circleView.tag = i; [self addSubview:circleView]; } } - (void)layoutSubviews { [super layoutSubviews]; CGFloat viewH = 80; CGFloat viewW = 80; int totalcol = 3; for (int i = 0; i < self.subviews.count; i++) { YUCircleView *btn = self.subviews[i]; CGFloat padding = (self.bounds.size.width - totalcol * viewW) / (totalcol + 1); CGFloat viewX = padding * (i % totalcol + 1) + i % totalcol * viewW; CGFloat viewY = padding * (i / totalcol + 1) + i / totalcol * viewW; btn.frame = CGRectMake(viewX, viewY, viewW, viewH); } }
在处理触摸事件的四个方法中经常要得到用户当前触摸的点,以及得到用户触摸到的按钮这两个功能,把他们单独抽出来做为方法比较好
- (CGPoint)getTouchPoint:(NSSet<UITouch *> *)touches { UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:touch.view]; self.currentPoint = point; return point; } - (YUCircleView *)getTouchBtn:(CGPoint)point { for (YUCircleView *btn in self.subviews) { CGFloat d = 50; CGFloat frameX = btn.center.x - d * 0.5; CGFloat frameY = btn.center.y - d * 0.5; if( CGRectContainsPoint(CGRectMake(frameX, frameY, d, d), point)) { return btn; } } return nil; }
触碰到圆心周围才判定触碰到按钮
于是触摸事件的方法如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { CGPoint currentPoint = [self getTouchPoint:touches]; YUCircleView *btn = [self getTouchBtn:currentPoint]; if (btn && btn.selected == NO) { btn.selected = YES; [self.selectedViews addObject:btn]; } [self setNeedsDisplay]; } - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { CGPoint currentPoint = [self getTouchPoint:touches]; YUCircleView *btn = [self getTouchBtn:currentPoint]; if (btn && btn.selected == NO) { btn.selected = YES; [self.selectedViews addObject:btn]; } [self setNeedsDisplay]; } - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event for (YUCircleView *btn in self.selectedViews) { btn.selected = NO; } [self.selectedViews removeAllObjects]; [self setNeedsDisplay]; } - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self touchesEnded:touches withEvent:event]; } - (void)drawRect:(CGRect)rect { if (self.selectedViews.count == 0) return; [[UIColor colorWithRed:32/255.0 green:210/255.0 blue:254/255.0 alpha:0.5] set]; UIBezierPath *path = [UIBezierPath bezierPath]; for (int i = 0; i < self.selectedViews.count; i++) { YUCircleView *btn = self.selectedViews[i]; if (i == 0) { [path moveToPoint:btn.center]; } else { [path addLineToPoint:btn.center]; } } [path addLineToPoint:self.currentPoint]; path.lineCapStyle = kCGLineCapRound; path.lineJoinStyle = kCGLineJoinBevel; path.lineWidth = 8; [path stroke]; }
drawRect方法中只要遍历所有选中的按钮再将他们的圆心依次相连就可以了,最后再连到用户当前触摸的点
这个解锁页面在用户画完后要拿到用户刚才画的路径进行操作的。
如何得到用户所画的路径呢?
这时候,初始化各个按钮给按钮绑定的tag就派上用场了,把每个选中的按钮的tag依次串成字符串就是路径啦,一般这个路径是要传出去的,由别人来操作,所以最好使用代理模式
首先声明代理协议
@class YULockView; @protocol YULockViewDelegate <NSObject> @optional - (void)lockView:(YULockView *)lockView didFinishPath:(NSString *)path; @end
在YULockView中加入代理属性
@interface YULockView : UIView @property(nonatomic, weak) IBOutlet id<YULockViewDelegate> delegate; @end
加入IBOutlet是为了我方便的连线设置代理
在touchesEnded方法中通知代理
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { // 通知代理 if ([self.delegate respondsToSelector:@selector(lockView:didFinishPath:)]) { NSMutableString *path = [NSMutableString string]; for (YUCircleView *btn in self.selectedViews) { [path appendFormat:@"%d", (int)btn.tag]; } [self.delegate lockView:self didFinishPath:path]; } for (YUCircleView *btn in self.selectedViews) { btn.selected = NO; } [self.selectedViews removeAllObjects]; [self setNeedsDisplay]; }
然后让控制器实现代理方法
@interface ViewController () <YULockViewDelegate> @end @implementation ViewController - (void)lockView:(YULockView *)lockView didFinishPath:(NSString *)path { NSLog(@"路径为:%@",path); } - (void)viewDidLoad { [super viewDidLoad]; } @end
随便画了一下控制台打印如下
2015-11-28 22:37:12.537 手势解锁-OC[835:45566] 路径为:13456780
接下来就用swift重新写了一遍
首先是自定义的button
class YUCircleView:UIButton { override init(frame: CGRect) { super.init(frame: frame) self.setup() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.setup() } func setup() { self.userInteractionEnabled = false; self.setImage(UIImage(named: "gesture_node_normal"), forState: .Normal) self.setImage(UIImage(named: "gesture_node_highlighted"), forState: .Selected) } }
接下来是自定义view的代码
public protocol YULockViewDelegate : NSObjectProtocol{ func didFinishPath(lockView:UIView,path:String) } class YULockView:UIView { weak var delegate:YULockViewDelegate? var selectedViews:[YUCircleView] = [] var currentPoint:CGPoint = CGPointZero func getTouchPoint(touches: Set<UITouch>) -> CGPoint { let touch = touches.first let point = touch?.locationInView(touch?.view) self.currentPoint = point! return point! } func getTouchBtn(point:CGPoint) -> YUCircleView? { for btn in self.subviews { let d:CGFloat = 50; let frameX = btn.center.x - d * 0.5; let frameY = btn.center.y - d * 0.5; if( CGRectContainsPoint(CGRectMake(frameX, frameY, d, d), point)) { return btn as? YUCircleView; } } return nil } override func drawRect(rect: CGRect) { if self.selectedViews.count == 0 {return} UIColor(colorLiteralRed: 32/255.0, green: 210/255.0, blue: 1.0, alpha: 0.5).set() let path = UIBezierPath.init() for var i = 0; i < self.selectedViews.count; i += 1 { let btn = self.selectedViews[i] if i == 0 { path.moveToPoint(btn.center) } else { path.addLineToPoint(btn.center) } } path.addLineToPoint(self.currentPoint) path.lineCapStyle = .Round; path.lineJoinStyle = .Bevel; path.lineWidth = 8; path.stroke() } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { let point = self.getTouchPoint(touches) let btn = self.getTouchBtn(point) if (btn != nil) && (btn!.selected == false) { btn!.selected = true self.selectedViews.append(btn!) } self.setNeedsDisplay() } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { let point = self.getTouchPoint(touches) let btn = self.getTouchBtn(point) if (btn != nil) && (btn!.selected == false) { btn!.selected = true self.selectedViews.append(btn!) } self.setNeedsDisplay() } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { if ((self.delegate?.respondsToSelector(Selector.init("didFinishPath:”))) != nil) { var path = "" for btn in self.selectedViews { path += "(btn.tag)" } self.delegate?.didFinishPath(self, path: path) } for item in self.selectedViews { item.selected = false } self.selectedViews.removeAll() self.setNeedsDisplay() } override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) { self.touchesEnded(touches!, withEvent: event) } override init(frame: CGRect) { super.init(frame: frame) self.setup() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.setup() } func setup() { for var i = 0; i < 9; i += 1 { let btn = YUCircleView(type: .Custom) btn.tag = i self.addSubview(btn) } } override func layoutSubviews() { super.layoutSubviews() let viewH:CGFloat = 80 let viewW:CGFloat = 80 let totalcol:Int = 3 for var i = 0; i < self.subviews.count; i += 1 { let col = i % totalcol let row = i / totalcol let paddingX = (self.bounds.size.width - CGFloat(totalcol) * viewW) / CGFloat(totalcol + 1) let paddingY = paddingX let viewX = paddingX + CGFloat(col) * (viewW + paddingX) let viewY = paddingY + CGFloat(row) * (viewH + paddingY) let btn = self.subviews[i] as! YUCircleView btn.frame = CGRectMake(viewX, viewY, viewW, viewH) } } }
总体来说思路是一样的,只是代码语法以及风格不同,个人偏爱swift一点,但是swift管类型比较严,不同类型的数据不能做运算,于是在layoutSubviews()方法中转换类型费了点劲,接着就是设置代理和实现代理方法了
class ViewController: UIViewController, YULockViewDelegate{ func didFinishPath(lockView: UIView, path: String) { print("路径为:"+path) } override func viewDidLoad() { super.viewDidLoad() for item in self.view.subviews { if item is YULockView { let lockView = item as! YULockView lockView.delegate = self } } } }
随便画了一下控制台输出如下
路径为:840123675