[PDF] iOS Scroll Viewプログラミングガイドを読んで、タップによるズーム機能が紹介されていたので、実際に作ってみたもののメモです。
実際のサンプルがgithubに公開されているので、そちらを見てみると分かりやすいと思います。
ここでは、ガイドラインを元に自分でコードを書いてみて気づいた点などを書いていきます。
##タップによるズーム
ピンチジェスチャに関しては基本的な実装をフレームワーク側で行っていますが、タップによるズーム(ダブルタップでズーム、など)を実装するにはアプリケーション側での実装が必要です。
####該当ビューのサブクラス
タップによるズームをさせたいビューをサブクラス化し、タッチに対しての制御を実装します。
また、いくつかのプロトコルも定義します。
#####SMPView.h
@protocol TapDetectingViewDelegate;
@interface SMPView : UIView
@property (assign, nonatmic) id<TapDetectingViewDelegate> delegate;
@end
@protocol TapDetectingViewDelegate <NSObject>
@optional
- (void)tapDetectingView:(SMPView *)view goSingleTapAtPoint:(CGPoint)tapPoint;
- (void)tapDetectingView:(SMPView *)view goDoubleTapAtPoint:(CGPoint)tapPoint;
- (void)tapDetectingView:(SMPView *)view goTwoFingerTapAtPoint:(CGPoint)tapPoint;
@end
プロトコルのメソッドは、このビューがタッチを識別し、 シングルタップ 、 ダブルタップ 、 2本指タップ を判断したあとにデリゲートに対してプロトコロで定義されたメソッドを呼び出します。
#####SMPView.m
@interface SMPView ()
@property (assign, nonatomic) BOOL multipleTouches;
@property (assign, nonatomic) BOOL twoFingerTapIsPossible;
@property (assign, nonatomic) CGPoint tapLocation;
- (void)handleSingleTap;
- (void)handleDoubleTap;
- (void)handleTwoFingerTap;
@end
multipleTouches
、twoFingerTapIsPossible
プロパティは、それぞれマルチタッチかどうか、2本指タップかどうかを表すBOOL値です。
タッチ開始時と終了時で適切にハンドリングし、デリゲートメソッドを呼び出すのに利用します。
@implementation SMPView
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// マルチタッチを有効化
self.multipleTouchEnabled = YES;
self.twoFingerTapIsPossible = NO;
self.multipleTouches = NO;
}
return self;
}
// ----------------------------------------------
// タッチ制御
// ----------------------------------------------
// タッチ開始処理
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// 保留中のhandleSingleTapメッセージがあればそれを取り消す
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(handleSingleTap)
object:nil];
// タッチの状態を更新
if ([[event touchesForView:self] count] > 1) {
self.multipleTouches = YES;
}
if ([[event touchesForView:self] count] > 2) {
// 指が3本以上触れていたら2本指タップの状態を`NO`に
self.twoFingerTapIsPossible = NO;
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// touches.countは話された指の数
// [event touchesForView:self].countは、直前までViewに触れていた指の数
BOOL allTouchesEnded = ([touches count] == [[event touchesForView:self] count]);
// まず単純なシングル/ダブルタップを判定する
// 複数回のタッチが行われていない場合のみ、シングル/ダブルタップの可能性がある
if (!self.multipleTouches) {
UITouch *touch = [touch anyObject];
self.tapLocation = [touch locationInView:self];
// シングルタップ
if ([touch tapCount] == 1) {
[self performSelector:@selector(handleSingleTap)
withObject:nil
afterDelay:0.35];
}
else if ([touch tapCount] == 2) {
[self handleDoubleTap];
}
}
// 複数回のタッチが行われていた場合は、それが2本指のタップだったかどうか、
// また、そうである状況が否定されていないか確認する
else if (self.multipleTouches && self.twoFingerTapIsPossible) {
// ケース1: 両方のタッチの同時の終了の場合
if ([touches count] == 2 && allTouchesEnded) {
int i = 0;
NSUinteger tapCounts[2];
CGPoint tapLocation[2];
for (UITouch *touch in touches) {
tapCount[i] = [touch tapCount];
tapLocation[i] = [touch locationInView:self];
i++;
}
if (tapCounts[0] == 1 && tapCounts[1] == 1) {
// 両方ともシングルタップであれば、これは2本指のタップ
self.tapLocation = midpointBetweenPoints(tapLocations[0], tapLocations[1]);
[self handleTwoFingerTap];
}
}
// ケース2: これが1回のタッチの終了で、もう1つはまだ終了していない場合
else if ([touches count] == 1 && !allTouchesEnded) {
UITouch *touch = [touches anyObject];
if ([touch tapCount] == 1) {
// タッチが1回のタップであれば、その位置を保存し、
// 2番目のタッチの位置と平均値を求められるようにする
self.tapLocation = [touch locationWithView:self];
}
else {
self.twoFingerTapIsPossible = NO;
}
}
// ケース3: これが2つのタッチの2番目の終了である場合
else if ([touches count] == 1 && allTouchesEnded) {
UITouch *touch = [touches anyObject];
if ([touch tapCount] == 1) {
// 最後のタッチがシングルタップであれば、これは2本指のタップ
self.tapLocation = midpointBetweenPoints(self.tapLocation, [touch locationInView:self]);
[self handleTwoFingerTap];
}
}
}
// すべてのタッチが終了した場合、タッチの監視状態をリセットする
if (allTouchesEnded) {
self.twoFingerTapIsPossible = YES;
self.multipleTouches = NO;
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
self.twoFingerTapIsPossible = YES;
self.multipleTouches = NO;
}
// ----------------------------------------------
// タップイベント制御
// ----------------------------------------------
- (void)handleSingleTap
{
if ([self.delegate respondsToSelector(@selector(tapDetectingView:goSingleTapAtPoint:)]) {
[self.delegate tapDetectingView:self goSingleTapAtPoint:self.tapLocation];
}
}
- (void)handleDoubleTap
{
if ([self.delegate respondsToSelector(@selector(tapDetectingView:goDoubleTapAtLocation:)]) {
[self.delegate tapDetectingView:self goDoubleTapAtLocation:self.tapLocation];
}
}
- (void)handleTwoFingerTap
{
if ([self.delegate respondsToSelector(@selector(tapDetectingView:goTwoFingerTapAtLocation:)]) {
[self.delegate tapDetectingView:self goTwoFingerTapAtPoint:self.tapLocation];
}
}
@end
// ----------------------------------------------
// ヘルパー関数
// ----------------------------------------------
CGPoint midpointBetweenPoints(CGPoint a, CGPoint b)
{
CGFloat x = (a.x + b.x) / 2.0;
CGFloat y = (a.y + b.y) / 2.0;
return CGPointMake(x, y);
}
##コントローラクラスでデリゲートを実装する
コントローラ側では、ScrollViewとズーム対象のビューを入れ子にして生成します。
その上で、ScrollViewのdelegate
プロパティに、自分自身をセットします。(つまり普通のdelegateの使い方です)
// SMPViewController.m
// デリゲートのみ記載
#define ZOOM_STEP 1.5
#pragma mark delegate methods
- (void)tapDetectingView:(SMPView *)view goSingleTapAtPoint:(CGPoint)tapPoint
{
// シングルタップは処理しない
}
// ダブルタップ時の処理
- (void)tapDetectingView:(SMPView *)view goDoubleTapAtPoint:(CGPoint)tapPoint
{
float newScale = self.scrollView.zoomScale / ZOOM_STEP;
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:tapPoint];
[self.scrollView zoomToRect:zoomRect animated:YES];
}
// 2本指タップ時の処理
- (void)tapDetectingView:(SMPView *)view goTwoFingerTapAtPoint:(CGPoint)tapPoint
{
float newScale = self.scrollView.zoomScale / ZOOM_STEP;
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:tapPoint];
[self.scrollView zoomToRect:zoomRect animated:YES];
}
#pragma mark helper function
- (CGRect)zoomRectForScale:(float)scale withCenter:(CGPoint)center
{
CGRect zoomRect;
// scaleで割ることで、ズーム対象となる矩形のサイズを決める
zoomRect.size.height = self.scrollView.frame.size.height / scale;
zoomRect.size.width = self.scrollView.frame.size.width / scale;
// 矩形の中心をズームの中心にする
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
return zoomRect;
}
###解説
基本的にどのデリゲートメソッドも、渡された位置を元に処理を行っています。
処理の中心になるのがzoomRectForScale:withCenter:
メソッドです。
これは、渡されたスケールと位置から、ズーム対象となる矩形を生成して返します。
zoomRectのイメージは、生成された矩形が元のスケールに戻るように計算される、と(自分は)イメージしています。
(つまり、小さい矩形なら、「それを元に戻す=拡大」、大きい矩形なら、「それを元に戻す=縮小」、というイメージ)
あとは生成された矩形を、ScrollViewのzoomToRect:animated:
メソッドに渡してやることでズームが実行されます。