Как точно знать, когда прокрутка UIScrollView прекратилась?

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

Я работал над горизонтальным подклассом UIScrollView (для iOS 4) с вкладками выбора. Одним из его требований является то, что он перестает прокручиваться ниже определенной скорости, чтобы быстрее взаимодействовать с пользователем. Он также должен привязать к началу вкладки. Другими словами, когда пользователь отпускает scrollview и его скорость низкая, он привязывается к позиции. Я реализовал это, и он работает, но там есть ошибка.

Что у меня сейчас:

Scrollview является его собственным делегатом. при каждом вызове scrollViewDidScroll: он обновляет свои переменные, связанные с скоростью:

-(void)refreshCurrentSpeed
{
    float currentOffset = self.contentOffset.x;
    NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];

    deltaOffset = (currentOffset - prevOffset);
    deltaTime = (currentTime - prevTime);    
    currentSpeed = deltaOffset/deltaTime;
    prevOffset = currentOffset;
    prevTime = currentTime;

    NSLog(@"deltaOffset is now %f, deltaTime is now %f and speed is %f",deltaOffset,deltaTime,currentSpeed);
}

Затем при необходимости происходит привязка:

-(void)snapIfNeeded
{
    if(canStopScrolling && currentSpeed <70.0f && currentSpeed>-70.0f)
    {
        NSLog(@"Stopping with a speed of %f points per second", currentSpeed);
        [self stopMoving];

        float scrollDistancePastTabStart = fmodf(self.contentOffset.x, (self.frame.size.width/3));
        float scrollSnapX = self.contentOffset.x - scrollDistancePastTabStart;
        if(scrollDistancePastTabStart > self.frame.size.width/6)
        {
            scrollSnapX += self.frame.size.width/3;
        }
        float maxSnapX = self.contentSize.width-self.frame.size.width;
        if(scrollSnapX>maxSnapX)
        {
            scrollSnapX = maxSnapX;
        }
        [UIView animateWithDuration:0.3
                         animations:^{self.contentOffset=CGPointMake(scrollSnapX, self.contentOffset.y);}
                         completion:^(BOOL finished){[self stopMoving];}
        ];
    }
    else
    {
        NSLog(@"Did not stop with a speed of %f points per second", currentSpeed);
    }
}

-(void)stopMoving
{
    if(self.dragging)
    {
        [self setContentOffset:CGPointMake(self.contentOffset.x, self.contentOffset.y) animated:NO];
    }
    canStopScrolling = NO;
}

Вот методы делегата:

-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    canStopScrolling = NO;
    [self refreshCurrentSpeed];
}

-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    canStopScrolling = YES;
    NSLog(@"Did end dragging");
    [self snapIfNeeded];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    [self refreshCurrentSpeed];
    [self snapIfNeeded];
}

Это работает хорошо в большинстве случаев, за исключением двух сценариев: 1. Когда пользователь прокручивается, не отпуская палец, и сразу после перемещения, он может двигаться в почти неподвижном положении, он часто привязывается к своему положению, как предполагалось, но много раз. Обычно требуется несколько попыток добиться этого. При отпускании появляются нечетные значения времени (очень низкие) и/или расстояния (довольно высокие), что приводит к высокоскоростному значению, в то время как scrollView в действительности почти или полностью неподвижен. 2. Когда пользователь удаляет прокрутку, чтобы остановить его перемещение, кажется, что scrollview устанавливает contentOffset в его предыдущее место. Эта телепортация приводит к очень высокому значению скорости. Это можно было бы исправить, проверяя, является ли предыдущая дельта текущей Delta * -1, но я предпочел бы более стабильное решение.

Я пробовал использовать didEndDecelerating, но когда происходит сбой, он не вызван. Это, вероятно, подтверждает, что он уже неподвижен. Кажется, что нет метода делегата, который вызывается, когда scrollview полностью перестает двигаться.

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

@interface  UIScrollView <UIScrollViewDelegate>
{
    bool canStopScrolling;
    float prevOffset;
    float deltaOffset; //remembered for debug purposes
    NSTimeInterval prevTime;
    NSTimeInterval deltaTime; //remembered for debug purposes
    float currentSpeed;
}

-(void)stopMoving;
-(void)snapIfNeeded;
-(void)refreshCurrentSpeed;

@end


@implementation TabScrollView

-(id) init
{
    self = [super init];
    if(self)
    {
        self.delegate = self;
        self.frame = CGRectMake(0.0f,0.0f,320.0f,40.0f);
        self.backgroundColor = [UIColor grayColor];
        float tabWidth = self.frame.size.width/3;
        self.contentSize = CGSizeMake(100*tabWidth, 40.0f);
        for(int i=0; i<100;i++)
        {
            UIView *view = [[UIView alloc] init];
            view.frame = CGRectMake(i*tabWidth,0.0f,tabWidth,40.0f);
            view.backgroundColor = [UIColor colorWithWhite:(float)(i%2) alpha:1.0f];
            [self addSubview:view];
        }
    }
    return self;
}

@end

Более короткая версия этого вопроса: как узнать, когда прокрутка просмотрела прокрутку? didEndDecelerating: не вызывается, когда вы отпускаете его неподвижно, didEndDragging: часто происходит во время прокрутки, и проверка скорости ненадежна из-за этого нечетного "прыжка", который устанавливает скорость на что-то случайное.

Ответ 1

Я нашел решение:

-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate

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

Для тех, кто не делает привязки, но должен знать, когда прокрутка остановлена, didEndDecelerating будет вызываться в конце перемещения прокрутки. В сочетании с проверкой willDecelerate в didEndDragging вы узнаете, когда прокрутка остановилась.

Ответ 2

[Отредактированный ответ] Это то, что я использую - он обрабатывает все случаи кросс. Вам нужен ивар, чтобы сохранить состояние, и, как показано в комментариях, есть другие способы справиться с этим.

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    //[super scrollViewWillBeginDragging:scrollView];   // pull to refresh

    self.isScrolling = YES;
    NSLog(@"+scrollViewWillBeginDragging");
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    //[super scrollViewDidEndDragging:scrollView willDecelerate:decelerate];    // pull to refresh

    if(!decelerate) {
        self.isScrolling = NO;
    }
    NSLog(@"%@scrollViewDidEndDragging", self.isScrolling ? @"" : @"-");
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    self.isScrolling = NO;
    NSLog(@"-scrollViewDidEndDecelerating");
}

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{   
    self.isScrolling = NO;
    NSLog(@"-scrollViewDidScrollToTop");
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
    self.isScrolling = NO;
    NSLog(@"-scrollViewDidEndScrollingAnimation");
}

Я создал действительно простой проект, используя вышеприведенный код, так что, когда человек взаимодействует с scrollView (включая WebView), он блокирует интенсивную работу процесса, пока пользователь не перестанет взаимодействовать с scrollView, и прокрутка просмотрела прекращение прокрутки. Это как 50 строк кода: ScrollWatcher

Ответ 3

Здесь, как объединить scrollViewDidEndDecelerating и scrollViewDidEndDragging:willDecelerate для выполнения некоторой операции при завершении прокрутки:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self stoppedScrolling];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView 
                  willDecelerate:(BOOL)decelerate
{
    if (!decelerate) {
        [self stoppedScrolling];
    }
}

- (void)stoppedScrolling
{
    // done, do whatever
}

Ответ 4

Делегированные методы, упомянутые в этом сообщении, не помогли мне. Я нашел еще один ответ, который обнаруживает конечный конец прокрутки в scrollViewDidScroll: Ссылка здесь

Ответ 5

Я ответил на этот вопрос в своем блоге, в котором описывается, как это сделать без проблем.

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

Как узнать, когда прокрутка UIScrollView

Это немного сложно, но я поставил там рецепт и подробно объяснил детали.