天天看點

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

概覽

iPhone的成功很大一部分得益于它多點觸摸的強大功能,喬布斯讓人們認識到手機其實是可以不用按鍵和手寫筆直接操作的,這不愧為一項偉大的設計。今天我們就針對iOS的觸摸事件(手勢操作)、運動事件、遠端控制事件等展開學習:

  1. iOS事件簡介
  2. 觸摸事件
  3. 手勢識别
  4. 運動事件
  5. 遠端控制事件

iOS事件

在iOS中事件分為三類:

  1. 觸摸事件:通過觸摸、手勢進行觸發(例如手指點選、縮放) 
  2. 運動事件:通過加速器進行觸發(例如手機晃動) 
  3. 遠端控制事件:通過其他遠端裝置觸發(例如耳機控制按鈕)

下圖是蘋果官方對于這三種事件的形象描述:

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

在iOS中并不是所有的類都能處理接收并事件,隻有繼承自UIResponder類的對象才能處理事件(如我們常用的UIView、UIViewController、UIApplication都繼承自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 NS_AVAILABLE_IOS(3_0); 運動開始時執行;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0); 運動結束後執行;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0); 運動被意外取消時執行;
遠端控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event NS_AVAILABLE_IOS(4_0); 接收到遠端控制消息時執行;

觸摸事件

基礎知識

三類事件中觸摸事件在iOS中是最常用的事件,這裡我們首先介紹觸摸事件。

在下面的例子中定義一個KCImage,它繼承于UIView,在KCImage中指定一個圖檔作為背景。定義一個視圖控制器KCTouchEventViewController,并且在其中聲明一個KCImage變量,添加到視圖控制器中。既然UIView和UIViewController都繼承于UIResponder,那麼也就就意味着所有的UIKit控件和視圖控制器均能接收觸摸事件。首先我們在KCTouchEventViewController中添加觸摸事件,并利用觸摸移動事件來移動KCImage,具體代碼如下:

//
//  KCTouchEvenViewController.m
//  TouchEventAndGesture
//
//  Created by Kenshin Cui on 14-3-16.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCTouchEvenViewController.h"
#import "KCImage.h"

@interface KCTouchEvenViewController (){
    KCImage *_image;
}

@end

@implementation KCTouchEvenViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _image=[[KCImage alloc]initWithFrame:CGRectMake(50, 50, 150, 169
                                                            )];
    //_image.userInteractionEnabled=NO;
    [self.view addSubview:_image];
}

#pragma mark - 視圖控制器的觸摸事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"UIViewController start touch...");
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    //取得一個觸摸對象(對于多點觸摸可能有多個對象)
    UITouch *touch=[touches anyObject];
    //NSLog(@"%@",touch);
    
    //取得目前位置
    CGPoint current=[touch locationInView:self.view];
    //取得前一個位置
    CGPoint previous=[touch previousLocationInView:self.view];
    
    //移動前的中點位置
    CGPoint center=_image.center;
    //移動偏移量
    CGPoint offset=CGPointMake(current.x-previous.x, current.y-previous.y);
    
    //重新設定新位置
    _image.center=CGPointMake(center.x+offset.x, center.y+offset.y);
    
    NSLog(@"UIViewController moving...");

}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"UIViewController touch end.");
}
@end      

現在運作程式:

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

上面示例中我們用到了UITouch類,當執行觸摸事件時會将這個對象傳入。在這個對象中包含了觸摸的所有資訊:

  • window:觸摸時所在的視窗 
  • view:觸摸時所在視圖 
  • tapCount:短時間内點選的次數 
  • timestamp:觸摸産生或變化的時間戳 
  • phase:觸摸周期内的各個狀态 
  • locationInView:方法:取得在指定視圖的位置 
  • previousLocationInView:方法:取得移動的前一個位置

從上面運作效果可以看到無論是選擇KCImage拖動還是在界面其他任意位置拖動都能達到移動圖檔的效果。既然KCImage是UIView當然在KCImage中也能觸發相應的觸摸事件,假設在KCImage中定義三個對應的事件:

//
//  KCImage.m
//  TouchEventAndGesture
//
//  Created by Kenshin Cui on 14-3-16.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCImage.h"

@implementation KCImage

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        UIImage *img=[UIImage imageNamed:@"photo.png"];
        [self setBackgroundColor:[UIColor colorWithPatternImage:img]];
    }
    return self;
}

#pragma mark - UIView的觸摸事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"UIView start touch...");
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"UIView moving...");
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"UIView touch end.");
}
@end      

此時如果運作程式會發現如果拖動KCImage無法達到預期的效果,但是可以發現此時會調用KCImage的觸摸事件而不會調用KCTouchEventViewController中的觸摸事件。如果直接拖拽其他空白位置則可以正常拖拽,而且從輸出資訊可以發現此時調用的是視圖控制器的觸摸事件。這是為什麼呢?要解答這個問題我們需要了解iOS中事件的處理機制。

事件處理機制

在iOS中發生觸摸後,事件會加入到UIApplication事件隊列(在這個系列關于iOS開發的第一篇文章中我們分析iOS程式原理的時候就說過程式運作後UIApplication會循環監聽使用者操作),UIApplication會從時間隊列取出最前面的事件并分發處理,通常先分發給應用程式主視窗,主視窗會調用hitTest:withEvent:方法(這是UIView的方法),根據該方法傳回的視圖進行事件處理(如果不重寫此方法,則它會選擇一個合适視圖執行);如果hitTest:withEvent:不處理受到的事件那麼此時就會傳遞給響應者鍊中下一個響應者,如果該響應者依然無法處理則重複這個過程傳給下下一個響應者,如果最後都沒處理那麼該事件被丢棄。

什麼是響應者鍊呢?我們知道在iOS程式中無論是最後面的UIWindow還是最前面的某個按鈕,它們的擺放是有前後關系的,一個控件可以放到另一個控件上面或下面,那麼使用者點選某個控件時是觸發上面的控件還是下面的控件呢,這種先後關系構成一個鍊條就叫“響應者鍊”。在iOS中響應者鍊的關系可以用下圖表示:

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

當一個事件發生後首先看initial view能否處理這個事件,如果不能則會将事件傳遞給其上級視圖(inital view的superView);如果上級視圖仍然無法處理則會繼續往上傳遞;一直傳遞到視圖控制器view controller,首先判斷視圖控制器的根視圖view是否能處理此事件;如果不能則接着判斷該視圖控制器能否處理此事件,如果還是不能則繼續向上傳遞;(對于第二個圖視圖控制器本身還在另一個視圖控制器中,則繼續交給父視圖控制器的根視圖,如果根視圖不能處理則交給父視圖控制器處理);一直到window,如果window還是不能處理此事件則繼續交給application(UIApplication單例對象)處理,如果最後application還是不能處理此事件則将其丢棄。

這個過程大家了解起來并不難,關鍵問題是在這個過程中各個對象如何知道自己能不能處理該事件呢?對于繼承UIResponder的對象,其不能處理事件有幾個條件:

  • userInteractionEnabled=NO 
  • hidden=YES 
  • alpha=0~0.01 
  • 沒有實作開始觸摸方法(注意是touchesBegan:withEvent:而不是移動和結束觸摸事件)

當然上面三個方法都是針對UIView控件或其子控件而言的,第四個方法可以針對UIView也可以針對視圖控制器等其他UIResponder子類。對于第四種情況這裡再次強調是對象中重寫了開始觸摸方法,則會處理這個事件,如果僅僅寫了移動、停止觸摸或取消觸摸事件(或者這三個事件都重寫了)沒有寫開始觸摸事件,則此事件該對象不會進行處理。

相信到了這裡大家對于上面點選圖檔為什麼不能拖拽已經很明确了。事實上通過前面的解釋大家應該可以猜到即使KCImage實作了開始拖拽方法,如果在KCTouchEventViewController中設定KCImage對象的userInteractionEnabled為NO也是可以拖拽的。

注意:上面提到hitTest:withEvent:可以指定觸發事件的視圖,這裡就不再舉例說明,這個方法重寫情況比較少,一般用于自定義手勢,有興趣的童鞋可以通路:Event Delivery: The Responder Chain。

手勢識别

簡介

通過前面的内容我們可以看到觸摸事件使用起來比較容易,但是對于多個手指觸摸并進行不同的變化操作就要複雜的多了。例如說如果兩個手指捏合,我們雖然在觸摸開始、移動等事件中可以通過UITouchs得到兩個觸摸對象,但是我們如何能判斷使用者是用兩個手指捏合還是橫掃或者拖動呢?在iOS3.2之後蘋果引入了手勢識别,對于使用者常用的手勢操作進行了識别并封裝成具體的類供開發者使用,這樣在開發過程中我們就不必再自己編寫算法識别使用者的觸摸操作了。在iOS中有六種手勢操作:

手勢 說明
UITapGestureRecognizer 點按手勢
UIPinchGestureRecognizer 捏合手勢
UIPanGestureRecognizer 拖動手勢
UISwipeGestureRecognizer 輕掃手勢,支援四個方向的輕掃,但是不同的方向要分别定義輕掃手勢
UIRotationGestureRecognizer 旋轉手勢
UILongPressGestureRecognizer 長按手勢

所有的手勢操作都繼承于UIGestureRecognizer,這個類本身不能直接使用。這個類中定義了這幾種手勢共有的一些屬性和方法(下表僅列出常用屬性和方法):

名稱 說明
屬性
@property(nonatomic,readonly) UIGestureRecognizerState state; 手勢狀态
@property(nonatomic, getter=isEnabled) BOOL enabled; 手勢是否可用
@property(nonatomic,readonly) UIView *view; 觸發手勢的視圖(一般在觸摸執行操作中我們可以通過此屬性獲得觸摸視圖進行操作)
@property(nonatomic) BOOL delaysTouchesBegan; 手勢識别失敗前不執行觸摸開始事件,預設為NO;如果為YES,那麼成功識别則不執行觸摸開始事件,失敗則執行觸摸開始事件;如果為NO,則不管成功與否都執行觸摸開始事件;
方法
- (void)addTarget:(id)target action:(SEL)action; 添加觸摸執行事件
- (void)removeTarget:(id)target action:(SEL)action; 移除觸摸執行事件
- (NSUInteger)numberOfTouches; 觸摸點的個數(同時觸摸的手指數)
- (CGPoint)locationInView:(UIView*)view;  在指定視圖中的相對位置
- (CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(UIView*)view; 觸摸點相對于指定視圖的位置
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer; 指定一個手勢需要另一個手勢執行失敗才會執行
代理方法
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer; 一個控件的手勢識别後是否阻斷手勢識别繼續向下傳播,預設傳回NO;如果為YES,響應者鍊上層對象觸發手勢識别後,如果下層對象也添加了手勢并成功識别也會繼續執行,否則上層對象識别後則不再繼續傳播;

手勢狀态

這裡着重解釋一下上表中手勢狀态這個對象。在六種手勢識别中,隻有一種手勢是離散手勢,它就是UITapGestureRecgnier。離散手勢的特點就是一旦識别就無法取消,而且隻會調用一次手勢操作事件(初始化手勢時指定的觸發方法)。換句話說其他五種手勢是連續手勢,連續手勢的特點就是會多次調用手勢操作事件,而且在連續手勢識别後可以取消手勢。從下圖可以看出兩者調用操作事件的次數是不同的:

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

在iOS中将手勢狀态分為如下幾種:

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    UIGestureRecognizerStatePossible,   // 尚未識别是何種手勢操作(但可能已經觸發了觸摸事件),預設狀态
    
    UIGestureRecognizerStateBegan,      // 手勢已經開始,此時已經被識别,但是這個過程中可能發生變化,手勢操作尚未完成
    UIGestureRecognizerStateChanged,    // 手勢狀态發生轉變
    UIGestureRecognizerStateEnded,      // 手勢識别操作完成(此時已經松開手指)
    UIGestureRecognizerStateCancelled,  // 手勢被取消,恢複到預設狀态
    
    UIGestureRecognizerStateFailed,     // 手勢識别失敗,恢複到預設狀态
    
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // 手勢識别完成,同UIGestureRecognizerStateEnded
};      
  • 對于離散型手勢UITapGestureRecgnizer要麼被識别,要麼失敗,點按(假設點按次數設定為1,并且沒有添加長按手勢)下去一次不松開則此時什麼也不會發生,松開手指立即識别并調用操作事件,并且狀态為3(已完成)。 
  • 但是連續手勢要複雜一些,就拿旋轉手勢來說,如果兩個手指點下去不做任何操作,此時并不能識别手勢(因為我們還沒旋轉)但是其實已經觸發了觸摸開始事件,此時處于狀态0;如果此時旋轉會被識别,也就會調用對應的操作事件,同時狀态變成1(手勢開始),但是狀态1隻有一瞬間;緊接着狀态變為2(因為我們的旋轉需要持續一會),并且重複調用操作事件(如果在事件中列印狀态會重複列印2);松開手指,此時狀态變為3,并調用1次操作事件。

為了大家更好的了解這個狀态的變化,不妨在操作事件中列印事件狀态,會發現在操作事件中的狀态永遠不可能為0(預設狀态),因為隻要調用此事件說明已經被識别了。前面也說過,手勢識别從根本還是調用觸摸事件而完成的,連續手勢之是以會發生狀态轉換完全是由于觸摸事件中的移動事件造成的,沒有移動事件也就不存在這個過程中狀态變化。

大家通過蘋果官方的分析圖再了解一下上面說的内容:

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

使用手勢

在iOS中添加手勢比較簡單,可以歸納為以下幾個步驟:

  1. 建立對應的手勢對象; 
  2. 設定手勢識别屬性【可選】; 
  3. 附加手勢到指定的對象; 
  4. 編寫手勢操作方法;

為了幫助大家了解,下面以一個圖檔檢視程式示範一下上面幾種手勢,在這個程式中我們完成以下功能:

如果點按圖檔會在導航欄顯示圖檔名稱;

如果長按圖檔會顯示删除按鈕,提示使用者是否删除;

如果捏合會放大、縮小圖檔;

如果輕掃會切換到下一張或上一張圖檔;

如果旋轉會旋轉圖檔;

如果拖動會移動圖檔;

具體布局草圖如下:

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

為了顯示導覽列,我們首先将主視圖控制器KCPhotoViewController放入一個導航控制器,然後在主視圖控制器中放一個UIImage用于展示圖檔。下面是主要代碼:

//
//  KCGestureViewController.m
//  TouchEventAndGesture
//
//  Created by Kenshin Cui on 14-3-16.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCPhotoViewController.h"
#define kImageCount 3

@interface KCPhotoViewController (){
    UIImageView *_imageView;//圖檔展示控件
    int _currentIndex;//目前圖檔索引
}

@end

@implementation KCPhotoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self initLayout];

    [self addGesture];
}


#pragma mark 布局
-(void)initLayout{
    /*添加圖檔展示控件*/
    CGSize screenSize=[UIScreen mainScreen].applicationFrame.size;
    CGFloat topPadding=20;
    CGFloat y=22+44+topPadding,height=screenSize.height-y-topPadding;
    
    CGRect imageFrame=CGRectMake(0, y, screenSize.width, height);
    _imageView=[[UIImageView alloc]initWithFrame:imageFrame];
    _imageView.contentMode=UIViewContentModeScaleToFill;//設定内容模式為縮放填充
    _imageView.userInteractionEnabled=YES;//這裡必須設定為YES,否則無法接收手勢操作
    [self.view addSubview:_imageView];
    
    //添加預設圖檔
    UIImage *image=[UIImage imageNamed:@"0.jpg"];
    _imageView.image=image;
    [self showPhotoName];
    
}

#pragma mark 添加手勢
-(void)addGesture{
    /*添加點按手勢*/
    //建立手勢對象
    UITapGestureRecognizer *tapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapImage:)];
    //設定手勢屬性
    tapGesture.numberOfTapsRequired=1;//設定點按次數,預設為1,注意在iOS中很少用輕按兩下操作
    tapGesture.numberOfTouchesRequired=1;//點按的手指數
    //添加手勢到對象(注意,這裡添加到了控制器視圖中,而不是圖檔上,否則點選空白無法隐藏導航欄)
    [self.view addGestureRecognizer:tapGesture];
    
    
    /*添加長按手勢*/
    UILongPressGestureRecognizer *longPressGesture=[[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressImage:)];
    longPressGesture.minimumPressDuration=0.5;//設定長按時間,預設0.5秒,一般這個值不要修改
    //注意由于我們要做長按提示删除操作,是以這個手勢不再添加到控制器視圖上而是添加到了圖檔上
    [_imageView addGestureRecognizer:longPressGesture];
    
    /*添加捏合手勢*/
    UIPinchGestureRecognizer *pinchGesture=[[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(pinchImage:)];
    [self.view addGestureRecognizer:pinchGesture];
    
    /*添加旋轉手勢*/
    UIRotationGestureRecognizer *rotationGesture=[[UIRotationGestureRecognizer alloc]initWithTarget:self action:@selector(rotateImage:)];
    [self.view addGestureRecognizer:rotationGesture];
    
    /*添加拖動手勢*/
    UIPanGestureRecognizer *panGesture=[[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panImage:)];
    [_imageView addGestureRecognizer:panGesture];
    
    /*添加輕掃手勢*/
    //注意一個輕掃手勢隻能控制一個方向,預設向右,通過direction進行方向控制
    UISwipeGestureRecognizer *swipeGestureToRight=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeImage:)];
    //swipeGestureToRight.direction=UISwipeGestureRecognizerDirectionRight;//預設為向右輕掃
    [self.view addGestureRecognizer:swipeGestureToRight];
    
    UISwipeGestureRecognizer *swipeGestureToLeft=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeImage:)];
    swipeGestureToLeft.direction=UISwipeGestureRecognizerDirectionLeft;
    [self.view addGestureRecognizer:swipeGestureToLeft];

}

#pragma mark 顯示圖檔名稱
-(void)showPhotoName{
    NSString *title=[NSString stringWithFormat:@"%i.jpg",_currentIndex];
    [self setTitle:title];
}

#pragma mark 下一張圖檔
-(void)nextImage{
    int index=(_currentIndex+kImageCount+1)%kImageCount;
    NSString *imageName=[NSString stringWithFormat:@"%i.jpg",index];
    _imageView.image=[UIImage imageNamed:imageName];
    _currentIndex=index;
    [self showPhotoName];
}

#pragma mark 上一張圖檔
-(void)lastImage{
    int index=(_currentIndex+kImageCount-1)%kImageCount;
    NSString *imageName=[NSString stringWithFormat:@"%i.jpg",index];
    _imageView.image=[UIImage imageNamed:imageName];
    _currentIndex=index;
    [self showPhotoName];
}

#pragma mark - 手勢操作
#pragma mark 點按隐藏或顯示導航欄
-(void)tapImage:(UITapGestureRecognizer *)gesture{
    //NSLog(@"tap:%i",gesture.state);
    BOOL hidden=!self.navigationController.navigationBarHidden;
    [self.navigationController setNavigationBarHidden:hidden animated:YES];
}

#pragma mark 長按提示是否删除
-(void)longPressImage:(UILongPressGestureRecognizer *)gesture{
    //NSLog(@"longpress:%i",gesture.state);
    //注意其實在手勢裡面有一個view屬性可以擷取點按的視圖
    //UIImageView *imageView=(UIImageView *)gesture.view;
    
    //由于連續手勢此方法會調用多次,是以需要判斷其手勢狀态
    if (gesture.state==UIGestureRecognizerStateBegan) {
        UIActionSheet *actionSheet=[[UIActionSheet alloc]initWithTitle:@"System Info" delegate:nil cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete the photo" otherButtonTitles:nil];
        [actionSheet showInView:self.view];

    }
}

#pragma mark 捏合時縮放圖檔
-(void)pinchImage:(UIPinchGestureRecognizer *)gesture{
    //NSLog(@"pinch:%i",gesture.state);
    
    if (gesture.state==UIGestureRecognizerStateChanged) {
        //捏合手勢中scale屬性記錄的縮放比例
        _imageView.transform=CGAffineTransformMakeScale(gesture.scale, gesture.scale);
    }else if(gesture.state==UIGestureRecognizerStateEnded){//結束後恢複
        [UIView animateWithDuration:.5 animations:^{
            _imageView.transform=CGAffineTransformIdentity;//取消一切形變
        }];
    }
}

#pragma mark 旋轉圖檔
-(void)rotateImage:(UIRotationGestureRecognizer *)gesture{
    //NSLog(@"rotate:%i",gesture.state);
    if (gesture.state==UIGestureRecognizerStateChanged) {
        //旋轉手勢中rotation屬性記錄了旋轉弧度
        _imageView.transform=CGAffineTransformMakeRotation(gesture.rotation);
    }else if(gesture.state==UIGestureRecognizerStateEnded){
        [UIView animateWithDuration:0.8 animations:^{
            _imageView.transform=CGAffineTransformIdentity;//取消形變
        }];
    }
}

#pragma mark 拖動圖檔
-(void)panImage:(UIPanGestureRecognizer *)gesture{
    if (gesture.state==UIGestureRecognizerStateChanged) {
        CGPoint translation=[gesture translationInView:self.view];//利用拖動手勢的translationInView:方法取得在相對指定視圖(這裡是控制器根視圖)的移動
        _imageView.transform=CGAffineTransformMakeTranslation(translation.x, translation.y);
    }else if(gesture.state==UIGestureRecognizerStateEnded){
        [UIView animateWithDuration:0.5 animations:^{
            _imageView.transform=CGAffineTransformIdentity;
        }];
    }
    
}

#pragma mark 輕掃則檢視下一張或上一張
//注意雖然輕掃手勢是連續手勢,但是隻有在識别結束才會觸發,不用判斷狀态
-(void)swipeImage:(UISwipeGestureRecognizer *)gesture{
//    NSLog(@"swip:%i",gesture.state);
//    if (gesture.state==UIGestureRecognizerStateEnded) {
    
        //direction記錄的輕掃的方向
        if (gesture.direction==UISwipeGestureRecognizerDirectionRight) {//向右
            [self nextImage];
//            NSLog(@"right");
        }else if(gesture.direction==UISwipeGestureRecognizerDirectionLeft){//向左
//            NSLog(@"left");
            [self lastImage];
        }
//    }
}


//-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
//    //NSLog(@"touch begin...");
//}
@end      

運作效果:

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

在上面示例中需要強調幾點:

  • UIImageView預設是不支援互動的,也就是userInteractionEnabled=NO ,是以要接收觸摸事件(手勢識别),必須設定userInteractionEnabled=YES(在iOS中UILabel、UIImageView的userInteractionEnabled預設都是NO,UIButton、UITextField、UIScrollView、UITableView等預設都是YES)。 
  • 輕掃手勢雖然是連續手勢但是它的操作事件隻會在識别結束時調用一次,其他連續手勢都會調用多次,一般需要進行狀态判斷;此外輕掃手勢支援四個方向,但是如果要支援多個方向需要添加多個輕掃手勢。

手勢沖突

細心的童鞋會發現在上面的示範效果圖中當切換到下一張或者上一張圖檔時并沒有輕掃圖檔而是在空白地方輕掃完成,原因是如果我輕掃圖檔會引起拖動手勢而不是輕掃手勢。換句話說,兩種手勢發生了沖突。

沖突的原因很簡單,拖動手勢的操作事件是在手勢的開始狀态(狀态1)識别執行的,而輕掃手勢的操作事件隻有在手勢結束狀态(狀态3)才能執行,是以輕掃手勢就作為了犧牲品沒有被正确識别。我們理想的情況當然是如果在圖檔上拖動就移動圖檔,如果在圖檔上輕掃就翻動圖檔。如何解決這個沖突呢?

在iOS中,如果一個手勢A的識别部分是另一個手勢B的子部分時,預設情況下A就會先識别,B就無法識别了。要解決這個沖突可以利用- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;方法來完成。正是前面表格中UIGestureRecognizer的最後一個方法,這個方法可以指定某個手勢執行的前提是另一個手勢失敗才會識别執行。也就是說如果我們指定拖動手勢的執行前提為輕掃手勢失敗就可以了,這樣一來當我們手指輕輕滑動時系統會優先考慮輕掃手勢,如果最後發現該操作不是輕掃,那麼就會執行拖動。隻要将下面的代碼添加到添加手勢之後就能解決這個問題了(注意為了更加清晰的區分拖動和輕掃[模拟器中拖動稍微快一點就識别成了輕掃],這裡将長按手勢的前提設定為拖動失敗,避免示範拖動時長按手勢會被識别):

//解決在圖檔上滑動時拖動手勢和輕掃手勢的沖突
    [panGesture requireGestureRecognizerToFail:swipeGestureToRight];
    [panGesture requireGestureRecognizerToFail:swipeGestureToLeft];
    //解決拖動和長按手勢之間的沖突
    [longPressGesture requireGestureRecognizerToFail:panGesture];      

運作效果:

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

兩個不同控件的手勢同時執行

我們知道在iOS的觸摸事件中,事件觸發是根據響應者鍊進行的,上層觸摸事件執行後就不再向下傳播。預設情況下手勢也是類似的,先識别的手勢會阻斷手勢識别操作繼續傳播。那麼如何讓兩個有層次關系并且都添加了手勢的控件都能正确識别手勢呢?答案就是利用代理的-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer方法。這個代理方法預設傳回NO,會阻斷繼續向下識别手勢,如果傳回YES則可以繼續向下傳播識别。

下面的代碼控制示範了當在圖檔上長按時同時可以識别控制器視圖的長按手勢(注意其中我們還控制了隻有在UIImageView中操作的手勢才能向下傳遞,如果不控制則所有控件都可以向下傳遞)

//
//  KCGestureViewController.m
//  TouchEventAndGesture
//
//  Created by Kenshin Cui on 14-3-16.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCPhotoViewController.h"
#define kImageCount 3

@interface KCPhotoViewController ()<UIGestureRecognizerDelegate>{
    UIImageView *_imageView;//圖檔展示控件
    int _currentIndex;//目前圖檔索引
}

@end

@implementation KCPhotoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self initLayout];

    [self addGesture];
}


#pragma mark 布局
-(void)initLayout{
    /*添加圖檔展示控件*/
    CGSize screenSize=[UIScreen mainScreen].applicationFrame.size;
    CGFloat topPadding=20;
    CGFloat y=22+44+topPadding,height=screenSize.height-y-topPadding;
    
    CGRect imageFrame=CGRectMake(0, y, screenSize.width, height);
    _imageView=[[UIImageView alloc]initWithFrame:imageFrame];
    _imageView.contentMode=UIViewContentModeScaleToFill;//設定内容模式為縮放填充
    _imageView.userInteractionEnabled=YES;//這裡必須設定位YES,否則無法接收手勢操作
    //_imageView.multipleTouchEnabled=YES;//支援多點觸摸,預設就是YES
    [self.view addSubview:_imageView];
    
    //添加預設圖檔
    UIImage *image=[UIImage imageNamed:@"0.jpg"];
    _imageView.image=image;
    [self showPhotoName];
    
}

#pragma mark 添加手勢
-(void)addGesture{
    /*添加點按手勢*/
    //建立手勢對象
    UITapGestureRecognizer *tapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapImage:)];
    //設定手勢屬性
    tapGesture.numberOfTapsRequired=1;//設定點按次數,預設為1,注意在iOS中很少用輕按兩下操作
    tapGesture.numberOfTouchesRequired=1;//點按的手指數
    //添加手勢到對象(注意,這裡添加到了控制器視圖中,而不是圖檔上,否則點選空白無法隐藏導航欄)
    [self.view addGestureRecognizer:tapGesture];
    
    
    /*添加長按手勢*/
    UILongPressGestureRecognizer *longPressGesture=[[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressImage:)];
    longPressGesture.minimumPressDuration=0.5;//設定長按時間,預設0.5秒,一般這個值不要修改
    //注意由于我們要做長按提示删除操作,是以這個手勢不再添加到控制器視圖上而是添加到了圖檔上
    [_imageView addGestureRecognizer:longPressGesture];
    
    /*添加捏合手勢*/
    UIPinchGestureRecognizer *pinchGesture=[[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(pinchImage:)];
    [self.view addGestureRecognizer:pinchGesture];
    
    /*添加旋轉手勢*/
    UIRotationGestureRecognizer *rotationGesture=[[UIRotationGestureRecognizer alloc]initWithTarget:self action:@selector(rotateImage:)];
    [self.view addGestureRecognizer:rotationGesture];
    
    /*添加拖動手勢*/
    UIPanGestureRecognizer *panGesture=[[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panImage:)];
    [_imageView addGestureRecognizer:panGesture];
    
    /*添加輕掃手勢*/
    //注意一個輕掃手勢隻能控制一個方向,預設向右,通過direction進行方向控制
    UISwipeGestureRecognizer *swipeGestureToRight=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeImage:)];
    //swipeGestureToRight.direction=UISwipeGestureRecognizerDirectionRight;//預設位向右輕掃
    [self.view addGestureRecognizer:swipeGestureToRight];
    
    UISwipeGestureRecognizer *swipeGestureToLeft=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeImage:)];
    swipeGestureToLeft.direction=UISwipeGestureRecognizerDirectionLeft;
    [self.view addGestureRecognizer:swipeGestureToLeft];
    
    //解決在圖檔上滑動時拖動手勢和輕掃手勢的沖突
    [panGesture requireGestureRecognizerToFail:swipeGestureToRight];
    [panGesture requireGestureRecognizerToFail:swipeGestureToLeft];
    //解決拖動和長按手勢之間的沖突
    [longPressGesture requireGestureRecognizerToFail:panGesture];
    
    
    /*示範不同視圖的手勢同時執行
     *在上面_imageView已經添加了長按手勢,這裡給視圖控制器的視圖也加上長按手勢讓兩者都執行
     *
     */
    self.view.tag=100;
    _imageView.tag=200;
    UILongPressGestureRecognizer *viewLongPressGesture=[[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressView:)];
    viewLongPressGesture.delegate=self;
    [self.view addGestureRecognizer:viewLongPressGesture];

}

#pragma mark 顯示圖檔名稱
-(void)showPhotoName{
    NSString *title=[NSString stringWithFormat:@"%i.jpg",_currentIndex];
    [self setTitle:title];
}

#pragma mark 下一張圖檔
-(void)nextImage{
    int index=(_currentIndex+kImageCount+1)%kImageCount;
    NSString *imageName=[NSString stringWithFormat:@"%i.jpg",index];
    _imageView.image=[UIImage imageNamed:imageName];
    _currentIndex=index;
    [self showPhotoName];
}

#pragma mark 上一張圖檔
-(void)lastImage{
    int index=(_currentIndex+kImageCount-1)%kImageCount;
    NSString *imageName=[NSString stringWithFormat:@"%i.jpg",index];
    _imageView.image=[UIImage imageNamed:imageName];
    _currentIndex=index;
    [self showPhotoName];
}

#pragma mark - 手勢操作
#pragma mark 點按隐藏或顯示導航欄
-(void)tapImage:(UITapGestureRecognizer *)gesture{
    //NSLog(@"tap:%i",gesture.state);
    BOOL hidden=!self.navigationController.navigationBarHidden;
    [self.navigationController setNavigationBarHidden:hidden animated:YES];
}

#pragma mark 長按提示是否删除
-(void)longPressImage:(UILongPressGestureRecognizer *)gesture{
    //NSLog(@"longpress:%i",gesture.state);
    //注意其實在手勢裡面有一個view屬性可以擷取點按的視圖
    //UIImageView *imageView=(UIImageView *)gesture.view;
    
    //由于連續手勢此方法會調用多次,是以需求判斷其手勢狀态
    if (gesture.state==UIGestureRecognizerStateBegan) {
        UIActionSheet *actionSheet=[[UIActionSheet alloc]initWithTitle:@"System Info" delegate:nil cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete the photo" otherButtonTitles:nil];
        [actionSheet showInView:self.view];

    }
}

#pragma mark 捏合時縮放圖檔
-(void)pinchImage:(UIPinchGestureRecognizer *)gesture{
    //NSLog(@"pinch:%i",gesture.state);
    
    if (gesture.state==UIGestureRecognizerStateChanged) {
        //捏合手勢中scale屬性記錄的縮放比例
        _imageView.transform=CGAffineTransformMakeScale(gesture.scale, gesture.scale);
    }else if(gesture.state==UIGestureRecognizerStateEnded){//結束後恢複
        [UIView animateWithDuration:.5 animations:^{
            _imageView.transform=CGAffineTransformIdentity;//取消一切形變
        }];
    }
}

#pragma mark 旋轉圖檔
-(void)rotateImage:(UIRotationGestureRecognizer *)gesture{
    //NSLog(@"rotate:%i",gesture.state);
    if (gesture.state==UIGestureRecognizerStateChanged) {
        //旋轉手勢中rotation屬性記錄了旋轉弧度
        _imageView.transform=CGAffineTransformMakeRotation(gesture.rotation);
    }else if(gesture.state==UIGestureRecognizerStateEnded){
        [UIView animateWithDuration:0.8 animations:^{
            _imageView.transform=CGAffineTransformIdentity;//取消形變
        }];
    }
}

#pragma mark 拖動圖檔
-(void)panImage:(UIPanGestureRecognizer *)gesture{
    if (gesture.state==UIGestureRecognizerStateChanged) {
        CGPoint translation=[gesture translationInView:self.view];//利用拖動手勢的translationInView:方法取得在相對指定視圖(控制器根視圖)的移動
        _imageView.transform=CGAffineTransformMakeTranslation(translation.x, translation.y);
    }else if(gesture.state==UIGestureRecognizerStateEnded){
        [UIView animateWithDuration:0.5 animations:^{
            _imageView.transform=CGAffineTransformIdentity;
        }];
    }
    
}

#pragma mark 輕掃則檢視下一張或上一張
//注意雖然輕掃手勢是連續手勢,但是隻有在識别結束才會觸發,不用判斷狀态
-(void)swipeImage:(UISwipeGestureRecognizer *)gesture{
//    NSLog(@"swip:%i",gesture.state);
//    if (gesture.state==UIGestureRecognizerStateEnded) {
    
        //direction記錄的輕掃的方向
        if (gesture.direction==UISwipeGestureRecognizerDirectionRight) {//向右
            [self nextImage];
//            NSLog(@"right");
        }else if(gesture.direction==UISwipeGestureRecognizerDirectionLeft){//向左
//            NSLog(@"left");
            [self lastImage];
        }
//    }
}



#pragma mark 控制器視圖的長按手勢
-(void)longPressView:(UILongPressGestureRecognizer *)gesture{
    NSLog(@"view long press!");
}


#pragma mark 手勢代理方法
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    //NSLog(@"%i,%i",gestureRecognizer.view.tag,otherGestureRecognizer.view.tag);
    
    //注意,這裡控制隻有在UIImageView中才能向下傳播,其他情況不允許
    if ([otherGestureRecognizer.view isKindOfClass:[UIImageView class]]) {
        return YES;
    }
    return NO;
}

#pragma mark - 觸摸事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"touch begin...");
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"touch end.");
}
@end      

運動事件

前面我們主要介紹了觸摸事件以及由觸摸事件引出的手勢識别,下面我們簡單介紹一下運動事件。在iOS中和運動相關的有三個事件:開始運動、結束運動、取消運動。

監聽運動事件對于UI控件有個前提就是監聽對象必須是第一響應者(對于UIViewController視圖控制器和UIAPPlication沒有此要求)。這也就意味着如果監聽的是一個UI控件那麼-(BOOL)canBecomeFirstResponder;方法必須傳回YES。同時控件顯示時(在-(void)viewWillAppear:(BOOL)animated;事件中)調用視圖控制器的becomeFirstResponder方法。當視圖不再顯示時(在-(void)viewDidDisappear:(BOOL)animated;事件中)登出第一響應者身份。

由于視圖控制器預設就可以調用運動開始、運動結束事件在此不再舉例。現在不妨假設我們現在在開發一個搖一搖找人的功能,這裡我們就自定義一個圖檔展示控件,在這個圖檔控件中我們可以通過搖晃随機切換界面圖檔。代碼比較簡單:

KCImageView.m

//
//  KCImageView.m
//  TouchEventAndGesture
//
//  Created by Kenshin Cui on 14-3-16.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCImageView.h"
#define kImageCount 3

@implementation KCImageView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.image=[self getImage];
    }
    return self;
}

#pragma mark 設定控件可以成為第一響應者
-(BOOL)canBecomeFirstResponder{
    return YES;
}

#pragma mark 運動開始
-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event{
    //這裡隻處理搖晃事件
    if (motion==UIEventSubtypeMotionShake) {
        self.image=[self getImage];
    }
}
#pragma mark 運動結束
-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event{
    
}


#pragma mark 随機取得圖檔
-(UIImage *)getImage{
    int index= arc4random()%kImageCount;
    NSString *imageName=[NSString stringWithFormat:@"avatar%i.png",index];
    UIImage *image=[UIImage imageNamed:imageName];
    return image;
}
@end      

KCShakeViewController.m

//
//  KCShakeViewController.m
//  TouchEventAndGesture
//
//  Created by Kenshin Cui on 14-3-16.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCShakeViewController.h"
#import "KCImageView.h"

@interface KCShakeViewController (){
    KCImageView *_imageView;
}

@end

@implementation KCShakeViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
}

#pragma mark 視圖顯示時讓控件變成第一響應者
-(void)viewDidAppear:(BOOL)animated{
    _imageView=[[KCImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
    _imageView.userInteractionEnabled=true;
    [self.view addSubview:_imageView];
    [_imageView becomeFirstResponder];
}

#pragma mark 視圖不顯示時登出控件第一響應者的身份
-(void)viewDidDisappear:(BOOL)animated{
    [_imageView resignFirstResponder];
}

/*視圖控制器的運動事件*/
//-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event{
//    NSLog(@"motion begin...");
//}
//
//-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event{
//    NSLog(@"motion end.");
//}

@end      

運作效果(下圖示範時使用了模拟器搖晃操作的快捷鍵,沒有使用滑鼠操作):

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

遠端控制事件

在今天的文章中還剩下最後一類事件:遠端控制,遠端控制事件這裡主要說的就是耳機線控操作。在前面的事件清單中,大家可以看到在iOS中和遠端控制事件有關的隻有一個- (void)remoteControlReceivedWithEvent:(UIEvent *)event NS_AVAILABLE_IOS(4_0);事件。要監聽到這個事件有三個前提(視圖控制器UIViewController或應用程式UIApplication隻有兩個)

  • 啟用遠端事件接收(使用[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];方法)。 
  • 對于UI控件同樣要求必須是第一響應者(對于視圖控制器UIViewController或者應用程式UIApplication對象監聽無此要求)。 
  • 應用程式必須是目前音頻的控制者,也就是在iOS 7中通知欄中目前音頻播放程式必須是我們自己開發程式。

基于第三點我們必須明确,如果我們的程式不想要控制音頻,隻是想利用遠端控制事件做其他的事情,例如模仿iOS7中的按音量+鍵拍照是做不到的,目前iOS7給我們的遠端控制權限還僅限于音頻控制(當然假設我們确實想要做一個和播放音頻無關的應用但是又想進行遠端控制,也可以隐藏一個音頻播放操作,拿到遠端控制操作權後進行遠端控制)。

運動事件中我們也提到一個枚舉類型UIEventSubtype,而且我們利用它來判斷是否運動事件,在枚舉中還包含了我們運程控制的子事件類型,我們先來熟悉一下這個枚舉(從遠端控制子事件類型也不難發現它和音頻播放有密切關系):

typedef NS_ENUM(NSInteger, UIEventSubtype) {
    // 不包含任何子事件類型
    UIEventSubtypeNone                              = 0,
    
    // 搖晃事件(從iOS3.0開始支援此事件)
    UIEventSubtypeMotionShake                       = 1,
    
    //遠端控制子事件類型(從iOS4.0開始支援遠端控制事件)
    //播放事件【操作:停止狀态下,按耳機線控中間按鈕一下】
    UIEventSubtypeRemoteControlPlay                 = 100,
    //暫停事件
    UIEventSubtypeRemoteControlPause                = 101,
    //停止事件
    UIEventSubtypeRemoteControlStop                 = 102,
    //播放或暫停切換【操作:播放或暫停狀态下,按耳機線控中間按鈕一下】
    UIEventSubtypeRemoteControlTogglePlayPause      = 103,
    //下一曲【操作:按耳機線控中間按鈕兩下】
    UIEventSubtypeRemoteControlNextTrack            = 104,
    //上一曲【操作:按耳機線控中間按鈕三下】
    UIEventSubtypeRemoteControlPreviousTrack        = 105,
    //快退開始【操作:按耳機線控中間按鈕三下不要松開】
    UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
    //快退停止【操作:按耳機線控中間按鈕三下到了快退的位置松開】
    UIEventSubtypeRemoteControlEndSeekingBackward   = 107,
    //快進開始【操作:按耳機線控中間按鈕兩下不要松開】
    UIEventSubtypeRemoteControlBeginSeekingForward  = 108,
    //快進停止【操作:按耳機線控中間按鈕兩下到了快進的位置松開】
    UIEventSubtypeRemoteControlEndSeekingForward    = 109,
};      

這裡我們将遠端控制事件放到視圖控制器(事實上很少直接添加到UI控件,一般就是添加到UIApplication或者UIViewController),模拟一個音樂播放器。

1.首先在應用程式啟動後設定接收遠端控制事件,并且設定音頻會話保證背景運作可以播放(注意要在應用配置中設定允許多任務)

//
//  AppDelegate.m
//  TouchEventAndGesture
//
//  Created by Kenshin Cui on 14-3-16.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "AppDelegate.h"
#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "KCApplication.h"

@interface AppDelegate ()

@end

@implementation AppDelegate
            

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    _window=[[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    
    _window.backgroundColor =[UIColor colorWithRed:249/255.0 green:249/255.0 blue:249/255.0 alpha:1];
    
    //設定全局導覽列風格和顔色
    [[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:23/255.0 green:180/255.0 blue:237/255.0 alpha:1]];
    [[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];

    ViewController *mainController=[[ViewController alloc]init];
    _window.rootViewController=mainController;
    
    //設定播放會話,在背景可以繼續播放(還需要設定程式允許背景運作模式)
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
    if(![[AVAudioSession sharedInstance] setActive:YES error:nil])
    {
        NSLog(@"Failed to set up a session.");
    }
    
    
    //啟用遠端控制事件接收
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
//    [self becomeFirstResponder];
     
    [_window makeKeyAndVisible];


    return YES;
}

//-(void)remoteControlReceivedWithEvent:(UIEvent *)event{
//    NSLog(@"remote");
//}

- (void)applicationWillResignActive:(UIApplication *)application {
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    
    [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

- (void)applicationWillTerminate:(UIApplication *)application {
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

@end      

2.在視圖控制器中添加遠端控制事件并音頻播放進行控制

//
//  ViewController.m
//  RemoteEvent
//
//  Created by Kenshin Cui on 14-3-16.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "ViewController.h"

@interface ViewController (){
    UIButton *_playButton;
    BOOL _isPlaying;
}

@end

@implementation ViewController
            
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self initLayout];
}

-(BOOL)canBecomeFirstResponder{
    return NO;
}

-(void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    _player = [[AVPlayer alloc] initWithURL:[NSURL URLWithString:@"http://stream.jewishmusicstream.com:8000"]];

    //[_player play];
    //_isPlaying=true;
}

#pragma mark 遠端控制事件
-(void)remoteControlReceivedWithEvent:(UIEvent *)event{
    NSLog(@"%i,%i",event.type,event.subtype);
    if(event.type==UIEventTypeRemoteControl){
        switch (event.subtype) {
            case UIEventSubtypeRemoteControlPlay:
                [_player play];
                _isPlaying=true;
                break;
            case UIEventSubtypeRemoteControlTogglePlayPause:
                if (_isPlaying) {
                    [_player pause];
                }else{
                    [_player play];
                }
                _isPlaying=!_isPlaying;
                break;
            case UIEventSubtypeRemoteControlNextTrack:
                NSLog(@"Next...");
                break;
            case UIEventSubtypeRemoteControlPreviousTrack:
                NSLog(@"Previous...");
                break;
            case UIEventSubtypeRemoteControlBeginSeekingForward:
                NSLog(@"Begin seek forward...");
                break;
            case UIEventSubtypeRemoteControlEndSeekingForward:
                NSLog(@"End seek forward...");
                break;
            case UIEventSubtypeRemoteControlBeginSeekingBackward:
                NSLog(@"Begin seek backward...");
                break;
            case UIEventSubtypeRemoteControlEndSeekingBackward:
                NSLog(@"End seek backward...");
                break;
            default:
                break;
        }
        [self changeUIState];
    }
}

#pragma mark 界面布局
-(void)initLayout{
    //專輯封面
    UIImage *image=[UIImage imageNamed:@"wxl.jpg"];
    UIImageView *imageView=[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
    imageView.image=image;
    imageView.contentMode=UIViewContentModeScaleAspectFill;
    [self.view addSubview:imageView];
    //播放控制台
    UIView *view=[[UIView alloc]initWithFrame:CGRectMake(0, 480, 320, 88)];
    view.backgroundColor=[UIColor lightGrayColor];
    view.alpha=0.9;
    [self.view addSubview:view];
    
    //添加播放按鈕
    _playButton=[UIButton buttonWithType:UIButtonTypeCustom];
    _playButton.bounds=CGRectMake(0, 0, 50, 50);
    _playButton.center=CGPointMake(view.frame.size.width/2, view.frame.size.height/2);
    [self changeUIState];
    [_playButton addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
    [view addSubview:_playButton];
}

#pragma mark 界面狀态
-(void)changeUIState{
    if(_isPlaying){
        [_playButton setImage:[UIImage imageNamed:@"playing_btn_pause_n.png"] forState:UIControlStateNormal];
        [_playButton setImage:[UIImage imageNamed:@"playing_btn_pause_h.png"] forState:UIControlStateHighlighted];
    }else{
        [_playButton setImage:[UIImage imageNamed:@"playing_btn_play_n.png"] forState:UIControlStateNormal];
        [_playButton setImage:[UIImage imageNamed:@"playing_btn_play_h.png"] forState:UIControlStateHighlighted];
    }
}

-(void)btnClick:(UIButton *)btn{
    if (_isPlaying) {
        [_player pause];
    }else{
        [_player play];
    }
    _isPlaying=!_isPlaying;
    [self changeUIState];
}
@end      

運作效果(真機截圖):

iOS:觸摸事件、手勢識别、搖晃事件、耳機線控 概覽 iOS事件 觸摸事件 手勢識别 運動事件 遠端控制事件

注意:

  • 為了模拟一個真實的播放器,程式中我們啟用了背景運作模式,配置方法:在info.plist中添加UIBackgroundModes并且添加一個元素值為audio。 
  • 即使利用線控進行音頻控制我們也無法監控到耳機增加音量、減小音量的按鍵操作(另外注意模拟器無法模拟遠端事件,請使用真機調試)。 
  • 子事件的類型跟目前音頻狀态有直接關系,點選一次播放/暫停按鈕究竟是【播放】還是【播放/暫停】狀态切換要看目前音頻處于什麼狀态,如果處于停止狀态則點選一下是播放,如果處于暫停或播放狀态點選一下是暫停和播放切換。 
  • 上面的程式已在真機調試通過,無論是線控還是點選應用按鈕都可以控制播放或暫停。