Подтвердить что ты не робот

Прокрутка двумя пальцами с помощью UIScrollView

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

Ответ 1

Вам нужно подклассифицировать UIScrollView (конечно!). Затем вам нужно:

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

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;

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

Ответ 4

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

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

Ответ 5

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

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

    // 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 тоже увеличивал масштаб... жесты не работают правильно, так как сжимать и масштабировать и прокручивать его. Я обновлю это, если найду подходящий ответ.

Ответ 6

нам удалось реализовать аналогичную функциональность в нашем приложении для рисования 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

Ответ 7

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

Довольно странное поведение заключается в том, что когда одно пальцевое событие отменяется жестом двух пальцев в течение разрешенного периода времени, таймер будет отложен. Он активируется после вызова touchCanceled Event. Поэтому у нас есть проблема, что мы пытаемся заблокировать масштабирование, хотя событие уже отменено и поэтому отключить масштабирование для следующего события. Чтобы справиться с этим поведением, метод обратного вызова таймера проверяет, были ли ранее вызваны вызовыCanceled. <Код >   @implementation JWTwoFingerScrollView

  #pragma mark -
#pragma mark Прохождение событий


- (id) initWithCoder: (NSCoder *) кодер {   self = [super initWithCoder: coder];   if (self) {       для (UIGestureRecognizer * r в 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;       }   }   вернуть себя;
}
- (void) lockZoomScale {   zoomScale [0] = self.minimumZoomScale;   zoomScale [1] = self.maximumZoomScale;   [self setMinimumZoomScale: self.zoomScale];   [self setMaximumZoomScale: self.zoomScale];       NSLog (@ "заблокировано%.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 (@ "разблокировано%.2f%.2f", self.minimumZoomScale, self.maximumZoomScale);   }
}

- (void) touchesBegan: (NSSet *) касается событияEvent: (UIEvent *) {   NSLog (@ "начал% i", [event allTouches].count);   [self setCanCancelContentTouches: YES];    if ([event allTouches].count == 1) {        touchhesBeganTimer = [NSTimer scheduleTimerWithTimeInterval: 0.1 target: self selector: @selector (firstTouchTimerFired:) userInfo: nil repeatats: NO];        [затрагиваетBeganTimer сохранить];        [touchFilter касаетсяBegan: затрагивает событиеEvent: event];    }
 }

// если одно касание пальцем отменяется двумя пальцами, этот таймер задерживается
// значит мы можем! используйте этот метод для отключения масштабирования, потому что он не вызван, когда требуются события с двумя пальцами; в противном случае мы бы отключили масштабирование при масштабировании
- (void) firstTouchTimerFired: (NSTimer *) таймер {   NSLog (@ "выстрелил" );   [self setCanCancelContentTouches: NO];   // если уже заблокировано: разблокировать   // это происходит, потому что два жесты пальца задерживают таймер до тех пор, пока событие touch не закончится.. тогда мы не хотим блокировать!   if (timerWasDelayed) {       [self unlockZoomScale];   }   else {       [self lockZoomScale];   }   timerWasDelayed = NO;
 }

- (void) touchesMoved: (NSSet *) касается событияEvent: (UIEvent *) {
//NSLog  (@ "move% i", [event allTouches].count);   [touchFilter touchesMoved: касается withEvent: event];
}

 - (void) touchesEnded: (NSSet *) касается событияEvent: (UIEvent *) {   NSLog (@ "закончился% i", [event allTouches].count);   [touchFilter touchesEnded: затрагивает событиеEvent: event];   [self unlockZoomScale];
 }

 // [self setCanCancelContentTouches: NO];
 - (void) touchCancelled: (NSSet *) касается событияEvent: (UIEvent *) {   NSLog (@ "отменено% i", [событие allTouches].count);   [touchFilter touchsCancelled: касается событияEvent: event];   [self unlockZoomScale];    timerWasDelayed = YES;
 }

@конец
Код>

Код >

Ответ 8

Плохая новость: iPhone SDK 3.0 и выше, не передавайте прикосновения к методам -touchesBegan: и - touchesEnded: ** UIScrollview **. Вы можете использовать методы touchesShouldBegin и touchesShouldCancelInContentView, которые не совпадают.

Если вы действительно хотите получить эти штрихи, у вас есть один хак, который позволяет это.

В вашем подклассе 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 суперкласса.

Ответ 9

Что я делаю, так это то, что мой контроллер просмотра настроил представление прокрутки:

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

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

Ответ 10

Кажется, это лучший ресурс для этого вопроса в Интернете. Еще одно близкое решение можно найти здесь.

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

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

  • A UIScrollView, содержащий ваш пользовательский вид

  • Масштабирование и панорамирование двумя пальцами (через UIPinchGestureRecognizer)

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

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

Создайте собственный подкласс UIScrollView и создайте этот прокрутка в этом пользовательском подклассе. Добавьте выход к контроллеру вида для прокрутки и подключите их. Теперь вы полностью настроены.

Вам нужно будет добавить следующий код в подкласс 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% и просто округляет его до этого. Вы можете настроить свой уровень допуска; Я видел это в режиме масштабирования страниц и думал, что это будет приятным прикосновением.

Ответ 11

Да, вам нужно подклассом UIScrollView и переопределить его методы - touchesBegan: и -touchesEnded:, чтобы передать штрихи вверх. Это, вероятно, также будет связано с подклассом, имеющим переменную-член UIView, чтобы он знал, что означает передавать штрихи до.

Ответ 12

Посмотрите решение:

#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

Это не задерживает первое касание и не останавливается, когда пользователь прикасается двумя пальцами после его использования. Тем не менее он позволяет отменить только что начатое одно касание с использованием таймера.

Ответ 13

Я поместил это в метод 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, есть просто оператор печати, чтобы проверить, что метод вводится →

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

НТН

Ответ 14

Kenshi ответьте в Swift 4

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