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

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个状态:
  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

 

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

posted @ 2015-11-29 16:05  yu3  阅读(1079)  评论(0编辑  收藏  举报