Прокрутка двумя пальцами с помощью UIScrollView - PullRequest
42 голосов
/ 24 апреля 2009

У меня есть приложение, в котором мой основной вид принимает и touchesBegan, и touchesMoved, и, следовательно, принимает касания одним пальцем и перетаскивает. Я хочу реализовать UIScrollView, и он у меня работает, но он переопределяет перетаскивания, и поэтому мой contentView никогда не получает их. Я хотел бы реализовать UIScrollview, где перетаскивание двумя пальцами указывает на прокрутку, а событие перетаскивания одним пальцем передается в мое представление содержимого, поэтому оно работает нормально. Нужно ли создавать свой собственный подкласс 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];
}

Ответы [ 14 ]

64 голосов
/ 04 июля 2010

В SDK 3.2 обработка касаний для UIScrollView обрабатывается с помощью распознавателей жестов.

Если вы хотите выполнять панорамирование двумя пальцами вместо стандартного панорамирования одним пальцем, вы можете использовать следующий код:

for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) {     
    if ([gestureRecognizer  isKindOfClass:[UIPanGestureRecognizer class]]) {
        UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *) gestureRecognizer;
        panGR.minimumNumberOfTouches = 2;               
    }
}
35 голосов
/ 04 августа 2013

Для iOS 5+ установка этого свойства имеет тот же эффект, что и ответ Майка Лоуренса:

self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;

Перетаскивание одним пальцем игнорируется panGestureRecognizer, поэтому событие перетаскивания одним пальцем передается в представление содержимого.

14 голосов
/ 25 сентября 2010

В iOS 3.2+ вы теперь можете довольно легко добиться прокрутки двумя пальцами. Просто добавьте распознаватель жестов панорамирования в представление прокрутки и установите его MaximumNumberOfTouches равным 1. Он будет запрашивать все прокрутки одним пальцем, но разрешить прокрутку 2+ пальцами по цепочке к встроенному распознавателю жестов панорамирования представления прокрутки (и, таким образом, разрешить нормальное поведение прокрутки).

UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(recognizePan:)];
panGestureRecognizer.maximumNumberOfTouches = 1;
[scrollView addGestureRecognizer:panGestureRecognizer];
[panGestureRecognizer release];
10 голосов
/ 10 мая 2009

Вам необходимо создать подкласс UIScrollView (конечно!). Тогда вам нужно:

  • сделать события одним пальцем, чтобы перейти к представлению вашего контента (легко), и

  • заставляет события двумя пальцами прокручивать представление прокрутки (может быть простым, может быть трудным, может быть невозможным).

Предложение Патрика, как правило, хорошо: пусть ваш подкласс UIScrollView знает о вашем представлении контента, затем в сенсорных обработчиках событий проверит количество пальцев и переадресует событие соответственно. Просто убедитесь, что (1) события, которые вы отправляете в представление содержимого, не возвращаются в UIScrollView через цепочку респондента (т.е. убедитесь, что обрабатываете их все), (2) учитывают обычный поток событий касания (то есть touchesBegan, чем некоторое количество {touchesBegan, touchesMoved, touchesEnded}, завершенных с помощью touchesEnded или touchesCancelled), особенно при работе с UIScrollView. # 2 может быть хитрым.

Если вы решите, что событие предназначено для UIScrollView, еще одна хитрость заключается в том, чтобы заставить UIScrollView поверить, что ваш жест двумя пальцами на самом деле является жестом одного пальца (поскольку UIScrollView нельзя прокручивать двумя пальцами). Попробуйте передать только данные для одного пальца в super (отфильтровав аргумент (NSSet *)touches - обратите внимание, что он содержит только измененные касания - и вообще игнорируя события для неправильного пальца).

Если это не сработает, у вас проблемы. Теоретически вы можете попытаться создать искусственные штрихи для подачи в UIScrollView, создав класс, похожий на UITouch. Базовый код C не проверяет типы, поэтому, возможно, приведение (YourTouch *) к (UITouch *) будет работать, и вы сможете обманным путем заставить UIScrollView обрабатывать касания, которых на самом деле не было.

Возможно, вы захотите прочитать мою статью о продвинутых хитростях UIScrollView (и увидеть там совершенно не связанный пример кода UIScrollView).

Конечно, если вы не можете заставить его работать, всегда есть возможность либо управлять движением UIScrollView вручную, либо использовать полностью написанное пользователем представление прокрутки. Есть класс TTScrollView в библиотеке Three20 ; пользователю это не нравится, но программисту приятно.

8 голосов
/ 09 марта 2011

Эти ответы беспорядок, поскольку вы можете найти правильный ответ, только прочитав все остальные ответы и комментарии (ближайший ответ получил вопрос в обратном направлении). Принятый ответ слишком расплывчат, чтобы быть полезным, и предлагает другой метод.

Синтез, это работает

    // 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;
        }
    }   

Для прокрутки требуются два пальца. Я сделал это в подклассе, но если нет, просто замените self.gestureRecognizers на myScrollView.gestureRecognizers, и все готово.

Единственное, что я добавил, это использование id, чтобы избежать уродливого каста:)

Это работает, но может стать довольно запутанным, если вы хотите, чтобы ваш UIScrollView тоже делал масштабирование ... жесты не работают должным образом, так как перетаскивание и масштабирование борются с этим. Я обновлю это, если найду подходящий ответ.

2 голосов
/ 28 декабря 2011

У меня есть дальнейшее улучшение кода выше. Проблема заключалась в том, что даже после того, как мы установили setCanCancelContentTouches:NO , у нас есть проблема, что жест масштабирования прервется с содержимым. Это не отменяет прикосновения к контенту, но в то же время позволяет изменять масштаб. Чтобы предотвратить это, я блокирую масштабирование, устанавливая значенияimumZoomScale и MaximumZoomScale на одинаковые значения каждый раз, когда срабатывает таймер.

Довольно странное поведение состоит в том, что, когда событие с одним пальцем отменяется жестом с двумя пальцами в течение разрешенного периода времени, таймер задерживается. Он запускается после вызова события 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

2 голосов
/ 10 марта 2011

нам удалось реализовать аналогичную функциональность в нашем приложении для рисования на iPhone, создав подкласс UIScrollView и отфильтровав события в зависимости от количества касаний простым и грубым способом:

//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;

Простое нажатие ничего не делает (вы можете обращаться с ним так, как вам нужно), коснитесь двумя пальцами прокрутки / масштабирования, как и ожидалось. GestureRecognizer не используется, поэтому работает с iOS 3.1

1 голос
/ 02 октября 2017

Я поместил это в метод viewDidLoad, и это завершает представление прокрутки, обрабатывающее поведение двух касаний и еще один обработчик жестов панорамирования, обрабатывающий поведение одного касания ->

scrollView.panGestureRecognizer.minimumNumberOfTouches = 2

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

scrollView.gestureRecognizers?.append(panGR)

и в методе handlePan, который является функцией, связанной с ViewController, есть просто оператор print для проверки того, что метод вводится ->

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

НТН

1 голос
/ 26 мая 2010

Похоже, это лучший ресурс для этого вопроса в интернете. Другое близкое решение можно найти здесь .

Я решил эту проблему весьма удовлетворительным образом, по сути, заменив свой собственный распознаватель жестов в уравнении. Я настоятельно рекомендую всем, кто пытается добиться эффекта, запрошенного оригинальным постером, рассмотреть эту альтернативу вместо агрессивного подкласса UIScrollView.

Следующий процесс обеспечит:

  • A UIScrollView с вашим пользовательским представлением

  • Увеличение и панорамирование двумя пальцами (через UIPinchGestureRecognizer)

  • Обработка событий вашего просмотра для всех остальных касаний

Во-первых, давайте предположим, что у вас есть контроллер вида и его вид. В IB сделайте представление подпредставлением scrollView и отрегулируйте правила изменения размера вашего представления так, чтобы оно не менялось. В атрибутах прокрутки включите все, что говорит "отказов" и выключите off"delaysContentTouches". Кроме того, для масштабирования min и max необходимо установить значение, отличное от значения по умолчанию, равного 1,0, поскольку, как утверждают документы Apple, это необходимо для увеличения.

Создайте пользовательский подкласс UIScrollView, и сделайте этот scrollview этим пользовательским подклассом. Добавьте выход к вашему контроллеру представления для 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];

Одна вещь, которая мне действительно понравилась, это то, что если вы посмотрите на последнюю строку, то заметите, что она захватывает любое окончательное масштабирование, которое составляет около 100%, и просто округляет его до этого. Вы можете настроить свой уровень терпимости; Я видел это в режиме масштабирования Страниц и думал, что это будет приятное прикосновение.

1 голос
/ 12 января 2010

Что мне нужно сделать, так это чтобы мой контроллер вида настроил представление прокрутки:

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

И в моем детском представлении у меня есть таймер, потому что касания двумя пальцами обычно начинаются как один палец, за которым быстро следуют два пальца.

- (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;
}

Если второе событие touchesBegan: поступает более чем одним пальцем, в представлении прокрутки можно отменить касания. Поэтому, если пользователь выполняет панорамирование двумя пальцами, это представление получит сообщение touchesCanceled:.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...