2014年2月21日 星期五

使用 iOS 內建的手勢辨識器

iOS 幫我們內建了好用的手勢辨識器:UIGestureRecognizer 類別。
有了它就不需要觀察所有的事件來查看手指如何移動。

接下來我們要實作--偵測 Swipe(滑動)

先來看看沒有使用手勢辨識器的程式碼:
#import "ViewController.h"

#define kMinimumGestureLength   25
#define kMaximumVariance        5

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UILabel *label;
@property CGPoint gestureStartPoint;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
    _label = nil;
}

#pragma mark - Added methods

- (void)eraseText {
    _label.text = @"";
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [touches anyObject];
    _gestureStartPoint = [touch locationInView:self.view];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [touches anyObject];
    CGPoint currentPosition = [touch locationInView:self.view];
    
    CGFloat deltaX = fabsf(_gestureStartPoint.x - currentPosition.x);
    CGFloat deltaY = fabsf(_gestureStartPoint.y - currentPosition.y);
    
    if (deltaX >= kMinimumGestureLength && deltaY <= kMaximumVariance) {
        
        _label.text = @"Horizontal swipe detected";
        [self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
        
    } else if (deltaY >= kMinimumGestureLength && deltaX <= kMaximumVariance) {
        
        _label.text = @"Vertical swipe detected";
        [self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
    }
}

@end

#define kMinimumGestureLength   25
#define kMaximumVariance        5

首先,我們先定義一個以像素計算的最小手勢長度,user 必須滑超過這個距離,手勢才會被當成 Swipe 計算。
然後再定義一個變異量,它是 user 偏離直線的最大距離,只要手勢偏離的程度不超過它就會被當成水平或垂直滑動來計算。

以上我們所定義的最小手勢長度是 25 像素、變異值是 5 像素。
只要 user 水平移動超過 25 像素,則手勢和起點的垂直位置上下最多可差到 5 像素。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    _gestureStartPoint = [touch locationInView:self.view];
}

以上method 主要是在抓取任何一個觸控,並儲存它的觸控點。
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [touches anyObject];
    CGPoint currentPosition = [touch locationInView:self.view];
    
    CGFloat deltaX = fabsf(_gestureStartPoint.x - currentPosition.x);
    CGFloat deltaY = fabsf(_gestureStartPoint.y - currentPosition.y);
    
    if (deltaX >= kMinimumGestureLength && deltaY <= kMaximumVariance) {
        
        _label.text = @"Horizontal swipe detected";
        [self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
        
    } else if (deltaY >= kMinimumGestureLength && deltaX <= kMaximumVariance) {
        
        _label.text = @"Vertical swipe detected";
        [self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
    }
}
UITouch *touch = [touches anyObject];
CGPoint currentPosition = [touch locationInView:self.view];

取得 user 手指目前的位置。

CGFloat deltaX = fabsf(_gestureStartPoint.x - currentPosition.x);
CGFloat deltaY = fabsf(_gestureStartPoint.y - currentPosition.y);

計算 user 手指從其起點位置向水平和垂直方向個移動多少距離。

fabsf()函式是來自C算術程式庫,它會回傳一個 float 的絕對值。在此的用意是讓我們兩個值相減而不用擔心出現負值。
     if (deltaX >= kMinimumGestureLength && deltaY <= kMaximumVariance) {
        
        _label.text = @"Horizontal swipe detected";
        [self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
        
    } else if (deltaY >= kMinimumGestureLength && deltaX <= kMaximumVariance) {
        
        _label.text = @"Vertical swipe detected";
        [self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
    }
檢查 user 是否在同一個方向移動超過最小值,且變異值小於另一個移動方向,已形成一個手勢。

[self performSelector:@selector(eraseText) withObject:nil afterDelay:2];

讓文字出現兩秒後將它清除。

手勢辨識器:
我們不會直接使用 UIGestureRecognizer,而是建立它的 subclass 的實體,這些實體每個都是為了特定的手勢所設計,
像是 Swipe、Tap、Pinch。

讓我們以手勢辨識器改寫上例程式:
#import "ViewController.h"

//#define kMinimumGestureLength   25
//#define kMaximumVariance        5

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UILabel *label;
//@property CGPoint gestureStartPoint;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UISwipeGestureRecognizer *vertical = [[UISwipeGestureRecognizer alloc] initWithTarget:self 
                                                                                   action:@selector(reportVerticalSwipe:)];
    vertical.direction = UISwipeGestureRecognizerDirectionUp | UISwipeGestureRecognizerDirectionDown;
    [self.view addGestureRecognizer:vertical];
    
    UISwipeGestureRecognizer *horizontal = [[UISwipeGestureRecognizer alloc] initWithTarget:self 
                                                                                     action:@selector(reportHorizontalSwipe:)];
    horizontal.direction = UISwipeGestureRecognizerDirectionLeft | UISwipeGestureRecognizerDirectionRight;
    [self.view addGestureRecognizer:horizontal];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    
    _label = nil;
}

#pragma mark - Added methods

- (void)eraseText {
    _label.text = @"";
}

/*
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [touches anyObject];
    _gestureStartPoint = [touch locationInView:self.view];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [touches anyObject];
    CGPoint currentPosition = [touch locationInView:self.view];
    
    CGFloat deltaX = fabsf(_gestureStartPoint.x - currentPosition.x);
    CGFloat deltaY = fabsf(_gestureStartPoint.y - currentPosition.y);
    
    if (deltaX >= kMinimumGestureLength && deltaY <= kMaximumVariance) {
        
        _label.text = @"Horizontal swipe detected";
        [self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
        
    } else if (deltaY >= kMinimumGestureLength && deltaX <= kMaximumVariance) {
        
        _label.text = @"Vertical swipe detected";
        [self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
    }
} */

- (void)reportHorizontalSwipe:(UIGestureRecognizer *)recognizer {

    _label.text = @"Horizontal swipe detected";
    [self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
}

- (void)reportVerticalSwipe:(UIGestureRecognizer *)recognizer {
    
    _label.text = @"Vertical swipe detected";
    [self performSelector:@selector(eraseText) withObject:nil afterDelay:2];
}

@end
首先,我們將 touchesBegan:withEvent:、touchesMoved:withEvent: 刪除,
新增了 reportHorizontalSwipe:、reportVerticalSwipe:。
這兩個 method 實作了滑動手勢的功能,就像之前刪除的 touchesBegan:withEvent:、touchesMoved:withEvent: 一樣。

接著,在 viewDidLoad 中新增程式碼:
    UISwipeGestureRecognizer *vertical = [[UISwipeGestureRecognizer alloc] initWithTarget:self 
                                                                                   action:@selector(reportVerticalSwipe:)];
    vertical.direction = UISwipeGestureRecognizerDirectionUp | UISwipeGestureRecognizerDirectionDown;
    [self.view addGestureRecognizer:vertical];
    
    UISwipeGestureRecognizer *horizontal = [[UISwipeGestureRecognizer alloc] initWithTarget:self 
                                                                                     action:@selector(reportHorizontalSwipe:)];
    horizontal.direction = UISwipeGestureRecognizerDirectionLeft | UISwipeGestureRecognizerDirectionRight;
    [self.view addGestureRecognizer:horizontal];

以上我們可以看到,藉由 UISwipeGestureRecognizer 我們只需要建立並且設定一些手勢辨識器,
並將它增加到 View 中,即可完成跟上一個範例同樣的功能。
當 user 使用其中一種手勢辨識器認得的方法和螢幕互動時,我們指定的方法就會被呼叫。