转载

一个java程序员自学IOS开发之路(十)

2015/11/26

Day 41

今天开始学起触摸事件

在用户使用 app过程中,会产生各种各样的事件

iOS中的事件可以分为 3大类型

一个java程序员自学IOS开发之路(十)

响应者对象

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个状态:
  1. 触摸开始:- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  2. 触摸移动:- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  3. 触摸结束:- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  4. 触摸取消(可能会经历):- (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

一个java程序员自学IOS开发之路(十)

很简单,先自定义一个 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

今天做了个手势解锁的页面

一个java程序员自学IOS开发之路(十)

每个圆圈都是一个 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

正文到此结束
Loading...