web-dev-qa-db-ja.com

UIScrollViewで2本の指を使ってスクロールする

メインビューがtouchesBegantouchesMovedの両方を受け入れるため、1本の指でタッチしてドラッグするアプリがあります。 UIScrollViewを実装したいのですが、機能していますが、ドラッグをオーバーライドするため、contentViewがドラッグを受け取りません。 UIScrollviewを実装したいのですが、2本指のドラッグがスクロールを示し、1本指のドラッグイベントがコンテンツビューに渡されるため、正常に実行されます。 UIScrollViewの独自のサブクラスを作成する必要がありますか?

これが私のappDelegateのコードで、UIScrollViewを実装しています。

@implementation MusicGridAppDelegate

@synthesize window;
@synthesize viewController;
@synthesize scrollView;


- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    // Override point for customization after app launch    
    //[application setStatusBarHidden:YES animated:NO];
    //[window addSubview:viewController.view];

    scrollView.contentSize = CGSizeMake(720, 480);
    scrollView.showsHorizontalScrollIndicator = YES;
    scrollView.showsVerticalScrollIndicator = YES;
    scrollView.delegate = self;
    [scrollView addSubview:viewController.view];
    [window makeKeyAndVisible];
}


- (void)dealloc {
    [viewController release];
    [scrollView release];
    [window release];
    [super dealloc];
}
42
Craig

UIScrollViewをサブクラス化する必要があります(もちろん!)。次に、次のことを行う必要があります。

  • 1本指のイベントを作成してコンテンツビューに移動する(簡単)

  • 2本指のイベントでスクロールビューをスクロールさせます(簡単かもしれませんが、難しいかもしれませんが、不可能かもしれません)。

Patrickの提案は、通常は問題ありません。UIScrollViewサブクラスにコンテンツビューを通知し、タッチイベントハンドラーで指の数を確認して、それに応じてイベントを転送します。 (1)コンテンツビューに送信するイベントがレスポンダーチェーンを通じてUIScrollViewにバブリングしないことを確認してください(つまり、すべてを処理するようにしてください)、(2)通常のタッチイベントのフロー(つまりtouchesBegan、いくつかの{touchesBegan、touchesMoved、touchesEnded}、touchesEndedまたはtouchesCancelledで終了)、特にUIScrollViewを処理する場合。 #2は注意が必要です。

イベントがUIScrollViewに対するものであると判断した場合、もう1つのトリックは、UIScrollViewに2本指ジェスチャーが実際には1本指ジェスチャーであると信じ込ませることです(UIScrollViewは2本の指でスクロールできないため)。 1つの指のデータのみをsuperに渡してみてください((NSSet *)touches引数—変更されたタッチのみが含まれることに注意してください—間違った指のイベントを完全に無視します)。

それがうまくいかない場合、あなたは困っています。理論的には、UITouchに似たクラスを作成することで、UIScrollViewにフィードする人工的なタッチを作成することができます。基礎となるCコードは型をチェックしないため、(YourTouch *)を(UITouch *)にキャストしても機能し、UIScrollViewをだまして、実際には発生しなかったタッチを処理させることができます。

あなたはおそらく読みたいと思います 高度なUIScrollViewトリックに関する私の記事 (そしてそこにまったく無関係のUIScrollViewサンプルコードをいくつか参照してください)。

もちろん、それを機能させることができない場合は、UIScrollViewの動きを手動で制御するか、完全にカスタム作成されたスクロールビューを使用するオプションが常にあります。 Three20ライブラリ ;にはTTScrollViewクラスがあります。それはユーザーにとっては気分が良くありませんが、プログラマにとっては気分が良いです。

10

SDK 3.2では、UIScrollViewのタッチ処理はジェスチャー認識機能を使用して処理されます。

デフォルトの1本指パンニングの代わりに2本指パンニングを実行する場合は、次のコードを使用できます。

for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) {     
    if ([gestureRecognizer  isKindOfClass:[UIPanGestureRecognizer class]]) {
        UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *) gestureRecognizer;
        panGR.minimumNumberOfTouches = 2;               
    }
}
64
Kenshi

IOS 5以降の場合、このプロパティを設定すると、Mike Laurenceの回答と同じ効果があります。

self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;

1本の指のドラッグはpanGestureRecognizerによって無視されるため、1本の指のドラッグイベントがコンテンツビューに渡されます。

39
Guto Araujo

IOS 3.2以降では、2本指のスクロールを非常に簡単に実行できるようになりました。パンジェスチャーレコグナイザーをスクロールビューに追加し、そのmaximumNumberOfTouchesを1に設定します。これにより、すべての1本指のスクロールが要求されますが、2本以上の指のスクロールでチェーンをスクロールビューの組み込みパンジェスチャーレコグナイザーに渡すことができます(したがって、通常のスクロール動作を許可します)。

UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(recognizePan:)];
panGestureRecognizer.maximumNumberOfTouches = 1;
[scrollView addGestureRecognizer:panGestureRecognizer];
[panGestureRecognizer release];
14
Mike Laurence

他のすべての回答とコメントを読むことによってのみ正しい回答を見つけることができるため、この回答は混乱します(最も近い回答は質問を逆に取得しました)。受け入れられた回答は曖昧すぎて役に立たず、別の方法を提案しています。

合成、これは機能します

    // makes it so that only two finger scrolls go
    for (id gestureRecognizer in self.gestureRecognizers) {     
        if ([gestureRecognizer  isKindOfClass:[UIPanGestureRecognizer class]])
        {
            UIPanGestureRecognizer *panGR = gestureRecognizer;
            panGR.minimumNumberOfTouches = 2;              
            panGR.maximumNumberOfTouches = 2;
        }
    }   

これには、スクロールに2本の指が必要です。私はこれをサブクラスで行いましたが、そうでない場合は、self.gestureRecognizersmyScrollView.gestureRecognizersそして、あなたは行ってもいいです。

私が追加した唯一のことは、醜いキャストを避けるためにidを使用することです:)

これは機能しますが、UIScrollViewでズームを実行したい場合は、かなり厄介になる可能性があります...ピンチツーズームとスクロールでそれがうまくいかないため、ジェスチャーが正しく機能しません。適切な答えを見つけてください。

8
Dan Rosenstark

上記のコードをさらに改善しました。問題は、setCanCancelContentTouches:NOを設定した後でも、ズームジェスチャがコンテンツで中断するという問題がありました。コンテンツのタッチはキャンセルされませんが、その間ズームできます。これを防ぐために、minimumZoomScaleとmaximumZoomScaleを毎回同じ値に設定してズームをロックすると、タイマーが起動します。

非常に奇妙な動作は、許可された時間内に2本の指のジェスチャーによって1本の指のイベントがキャンセルされると、タイマーが遅延することです。 touchCanceledイベントが呼び出された後に発生します。したがって、イベントが既にキャンセルされているにもかかわらずズームをロックしようとするため、次のイベントのズームを無効にするという問題があります。この動作を処理するために、タイマーコールバックメソッドはtouchesCanceledが以前に呼び出されたかどうかをチェックします。 @implementation JWTwoFingerScrollView

#pragma mark -
#pragma mark Event Passing


- (id)initWithCoder:(NSCoder *)coder {
    self = [super initWithCoder:coder];
    if (self) {
        for (UIGestureRecognizer* r in self.gestureRecognizers) {
            if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
                [((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
                [((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
                zoomScale[0] = -1.0;
                zoomScale[1] = -1.0;
            }
            timerWasDelayed = NO;
        }
    }
    return self;
}
-(void)lockZoomScale {    
    zoomScale[0] = self.minimumZoomScale;
    zoomScale[1] = self.maximumZoomScale;
    [self setMinimumZoomScale:self.zoomScale];
    [self setMaximumZoomScale:self.zoomScale];
        NSLog(@"locked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
}
-(void)unlockZoomScale {
    if (zoomScale[0] != -1 && zoomScale[1] != -1) {
        [self setMinimumZoomScale:zoomScale[0]];
        [self setMaximumZoomScale:zoomScale[1]];
        zoomScale[0] = -1.0;
        zoomScale[1] = -1.0;
        NSLog(@"unlocked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
    }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"began %i",[event allTouches].count);
    [self setCanCancelContentTouches:YES];
     if ([event allTouches].count == 1){
         touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
         [touchesBeganTimer retain];
         [touchFilter touchesBegan:touches withEvent:event];
     }
 }

//if one finger touch gets canceled by two finger touch, this timer gets delayed
// so we can! use this method to disable zooming, because it doesnt get called when two finger touch events are wanted; otherwise we would disable zooming while zooming
-(void)firstTouchTimerFired:(NSTimer*)timer {
    NSLog(@"fired");
    [self setCanCancelContentTouches:NO];
    //if already locked: unlock
    //this happens because two finger gesture delays timer until touch event finishes.. then we dont want to lock!
    if (timerWasDelayed) {
        [self unlockZoomScale];
    }
    else {
        [self lockZoomScale];
    }
    timerWasDelayed = NO;
 }

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
//    NSLog(@"moved %i",[event allTouches].count);
    [touchFilter touchesMoved:touches withEvent:event];
}

 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"ended %i",[event allTouches].count);
    [touchFilter touchesEnded:touches withEvent:event];
    [self unlockZoomScale];
 }

 //[self setCanCancelContentTouches:NO];
 -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"canceled %i",[event allTouches].count);
    [touchFilter touchesCancelled:touches withEvent:event];
    [self unlockZoomScale];
     timerWasDelayed = YES;
 }

@end
 </ code>
2
Jan

私たちは、UIScrollViewをサブクラス化し、タッチの数に応じてイベントを単純かつ無礼な方法でフィルタリングすることにより、iPhone描画アプリに同様の機能を実装することに成功しました。

//OCRScroller.h
@interface OCRUIScrollView: UIScrollView
{
    double pass2scroller;
}
@end

//OCRScroller.mm
@implementation OCRUIScrollView
- (id)initWithFrame:(CGRect)aRect {
    pass2scroller = 0;
    UIScrollView* newv = [super initWithFrame:aRect];
    return newv;
}
- (void)setupPassOnEvent:(UIEvent *)event {
    int touch_cnt = [[event allTouches] count];
    if(touch_cnt<=1){
        pass2scroller = 0;
    }else{
        double timems = double(CACurrentMediaTime()*1000);
        pass2scroller = timems+200;
    }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self setupPassOnEvent:event];
    [super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [self setupPassOnEvent:event];
    [super touchesMoved:touches withEvent:event];   
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    pass2scroller = 0;
    [super touchesEnded:touches withEvent:event];
}


- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view
{
    return YES;
}

- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
    double timems = double(CACurrentMediaTime()*1000);
    if (pass2scroller == 0 || timems> pass2scroller){
        return NO;
    }
    return YES;
}
@end

次のように設定されたScrollView:

scroll_view = [[OCRUIScrollView alloc] initWithFrame:rect];
scroll_view.contentSize = img_size;
scroll_view.contentOffset = CGPointMake(0,0);
scroll_view.canCancelContentTouches = YES;
scroll_view.delaysContentTouches = NO;
scroll_view.scrollEnabled = YES;
scroll_view.bounces = NO;
scroll_view.bouncesZoom = YES;
scroll_view.maximumZoomScale = 10.0f;
scroll_view.minimumZoomScale = 0.1f;
scroll_view.delegate = self;
self.view = scroll_view;

単純なタップは何もしません(必要な方法で処理できます)。2本指でタップすると、期待どおりにスクロール/ズームビューが表示されます。 GestureRecognizerは使用されないため、iOS 3.1から機能します

2
IPv6

悪い知らせ:iPhone SDK 3.0以降、-touchesBegan:および-touchesEnded: ** UIScrollview ** subclassメソッドにタッチを渡さないでください。同じではないtouchesShouldBeginメソッドとtouchesShouldCancelInContentViewメソッドを使用できます。

本当にこのタッチを取得したい場合は、これを許可するhackを1つ用意してください。

UIScrollViewのサブクラスで、次のようにhitTestメソッドをオーバーライドします。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

  UIView *result = nil;
  for (UIView *child in self.subviews)
    if ([child pointInside:point withEvent:event])
      if ((result = [child hitTest:point withEvent:event]) != nil)
        break;

  return result;
}

これにより、このタッチのサブクラスが渡されますが、UIScrollViewスーパークラスへのタッチをキャンセルすることはできません。

これをviewDidLoadメソッドに配置すると、2つのタッチパン動作を処理するスクロールビューと、1つのタッチパン動作を処理する別のパンジェスチャーハンドラーが完成します->

scrollView.panGestureRecognizer.minimumNumberOfTouches = 2

let panGR = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGR.minimumNumberOfTouches = 1
panGR.maximumNumberOfTouches = 1

scrollView.gestureRecognizers?.append(panGR)

そして、ViewControllerに接続された関数であるhandlePanメソッドには、メソッドが入力されていることを確認するためのprintステートメントがあります->

@IBAction func handlePan(_ sender: UIPanGestureRecognizer) {
    print("Entered handlePan numberOfTuoches: \(sender.numberOfTouches)")
}

HTH

1
Adam Freeman

私がしていることは、ビューコントローラーにスクロールビューをセットアップさせることです:

[scrollView setCanCancelContentTouches:NO];
[scrollView setDelaysContentTouches:NO];

そして、私の子供の見解では、2本の指のタッチは通常1本の指に続いて2本の指が続くことから始まるので、タイマーがあります。

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // Hand tool or two or more touches means a pan or zoom gesture.
    if ((selectedTool == kHandToolIndex) || (event.allTouches.count > 1)) {
        [[self parentScrollView] setCanCancelContentTouches:YES];
        [firstTouchTimer invalidate];
        firstTouchTimer = nil;
        return;
    }

    // Use a timer to delay first touch because two-finger touches usually start with one touch followed by a second touch.
    [[self parentScrollView] setCanCancelContentTouches:NO];
    anchorPoint = [[touches anyObject] locationInView:self];
    firstTouchTimer = [NSTimer scheduledTimerWithTimeInterval:kFirstTouchTimeInterval target:self selector:@selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
    firstTouchTimeStamp = event.timestamp;
}

2番目のtouchesBegan:イベントが複数の指で発生した場合、スクロールビューはタッチをキャンセルできます。したがって、ユーザーが2本の指を使用して画面移動すると、このビューはtouchesCanceled:メッセージ。

1
lucius

これは、インターネット上のこの質問に最適なリソースのようです。別の近い解決策 ここで見つけることができます

この問題を別の方法で非常に満足できる方法で解決しました。本質的には、自分のジェスチャー認識機能を数式に置き換えることです。元の投稿者が要求した効果を達成しようとしている人は、UIScrollViewの積極的なサブクラス化よりもこの代替案を検討することを強くお勧めします。

次のプロセスが提供されます:

  • カスタムビューを含むUIScrollView

  • 2本の指でズームおよびパン(UIPinchGestureRecognizerを使用)

  • 他のすべてのタッチに対するビューのイベント処理

まず、ビューコントローラとそのビューがあるとしましょう。 IBで、ビューをscrollViewのサブビューにして、サイズ変更しないようにビューのサイズ変更ルールを調整します。スクロールビューの属性で、「バウンス」と言うものをすべてオンにし、offdelaysContentTouches」をオンにします。また、ズームの最小値と最大値をデフォルトの1.0以外に設定する必要があります。これは、Appleのドキュメントにあるように、ズームが機能するために必要です。

UIScrollViewのカスタムサブクラスを作成し、このスクロールビューをそのカスタムサブクラスにします。 scrollviewのビューコントローラにアウトレットを追加し、それらを接続します。これで完全に構成されました。

次のコードをUIScrollViewサブクラスに追加して、透過的にタッチイベントを渡すようにする必要があります(これは、おそらくエレガントに、おそらくサブクラス全体をバイパスすることもできるでしょう)。

#pragma mark -
#pragma mark Event Passing

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.nextResponder touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.nextResponder touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.nextResponder touchesEnded:touches withEvent:event];
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
    return NO;
}

このコードをビューコントローラに追加します。

- (void)setupGestures {
    UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
    [self.view addGestureRecognizer:pinchGesture];
    [pinchGesture release];
}

- (IBAction)handlePinchGesture:(UIPinchGestureRecognizer *)sender {
if ( sender.state == UIGestureRecognizerStateBegan ) {
    //Hold values
    previousLocation = [sender locationInView:self.view];
    previousOffset = self.scrollView.contentOffset;
    previousScale = self.scrollView.zoomScale;
} else if ( sender.state == UIGestureRecognizerStateChanged ) {
    //Zoom
    [self.scrollView setZoomScale:previousScale*sender.scale animated:NO];

    //Move
    location = [sender locationInView:self.view];
    CGPoint offset = CGPointMake(previousOffset.x+(previousLocation.x-location.x), previousOffset.y+(previousLocation.y-location.y));
    [self.scrollView setContentOffset:offset animated:NO];  
} else {
    if ( previousScale*sender.scale < 1.15 && previousScale*sender.scale > .85 )
        [self.scrollView setZoomScale:1.0 animated:YES];
}

}

このメソッドには、ビューコントローラーのクラスファイルで定義する必要があるいくつかのプロパティへの参照があることに注意してください。

  • CGFloat previousScale;
  • CGPoint previousOffset;
  • CGPoint previousLocation;
  • CGPoint location;

了解です。

残念ながら、ジェスチャーの実行中にスクローラーを表示するscrollViewを取得することはできません。私はこれらの戦略をすべて試しました:

//Scroll indicators
self.scrollView.showsVerticalScrollIndicator = YES;
self.scrollView.showsVerticalScrollIndicator = YES;
[self.scrollView flashScrollIndicators];
[self.scrollView setNeedsDisplay];

私が本当に楽しんだことの1つは、最後の行を見ると、100%前後の最終的なズームをすべて取得し、それに丸めるだけであることがわかります。許容レベルを調整できます。 Pagesのズーム動作でこれを見たことがあって、いい感じだと思った。

1
SG1

ケンシ の回答Swift 4

for gestureRecognizer: UIGestureRecognizer in self.gestureRecognizers! {
    if (gestureRecognizer is UIPanGestureRecognizer) {
        let panGR = gestureRecognizer as? UIPanGestureRecognizer
        panGR?.minimumNumberOfTouches = 2
    }
}
0
tnylee

私のチェックアウト ソリューション

#import “JWTwoFingerScrollView.h”

@implementation JWTwoFingerScrollView

- (id)initWithCoder:(NSCoder *)coder {
    self = [super initWithCoder:coder];
    if (self) {
        for (UIGestureRecognizer* r in self.gestureRecognizers) {
            NSLog(@“%@”,[r class]);
            if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
                [((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
                [((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
            }
        }
    }
    return self;
}

-(void)firstTouchTimerFired:(NSTimer*)timer {
    [self setCanCancelContentTouches:NO];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self setCanCancelContentTouches:YES];
    if ([event allTouches].count == 1){
        touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(firstTouchTimerFired:) userInfo: nil repeats:NO];
        [touchesBeganTimer retain];
        [touchFilter touchesBegan:touches withEvent:event];
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [touchFilter touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@“ended %i”,[event allTouches].count);
    [touchFilter touchesEnded:touches withEvent:event];
}

-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@“canceled %i”,[event allTouches].count);
    [touchFilter touchesCancelled:touches withEvent:event];
}

@end

最初のタッチを遅らせたり、ユーザーが1本の指を使用した後に2本の指でタッチしても停止しません。それでも、タイマーを使用して、開始したばかりのワンタッチイベントをキャンセルできます。

0
Jan

はい、タッチを「上」に渡すには、UIScrollViewをサブクラス化し、その-touchesBegan:および-touchesEnded:メソッドをオーバーライドする必要があります。これには、UIViewメンバー変数を持つサブクラスも含まれるため、タッチを渡すことの意味を知ることができます。

0
anisoptera