web-dev-qa-db-fra.com

Comment savoir exactement quand le défilement d'un UIScrollView s'est arrêté?

En bref, j'ai besoin de savoir exactement quand la vue défilement a cessé de défiler. Par 'arrêt du défilement', j'entends le moment où il ne bouge plus et ne se fait plus toucher.

J'ai travaillé sur une sous-classe horizontale UIScrollView (pour iOS 4) contenant des onglets de sélection. Une de ses exigences est qu’il arrête de faire défiler en dessous d’une certaine vitesse pour permettre une interaction plus rapide de l’utilisateur. Il devrait également s'aligner au début d'un onglet. En d’autres termes, lorsque l’utilisateur relâche le scrollview et que sa vitesse est faible, il se fixe sur une position. J'ai implémenté ceci et cela fonctionne, mais il y a un bug.

Ce que j'ai maintenant:

Scrollview est son propre délégué. à chaque appel à scrollViewDidScroll :, il actualise ses variables liées à la vitesse:

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

Procède ensuite à la capture si nécessaire:

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

Voici les méthodes déléguées:

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

Cela fonctionne bien la plupart du temps, sauf dans deux scénarios: 1. Lorsque l'utilisateur fait défiler son doigt sans relâcher son doigt et se laisse aller à un moment presque stationnaire juste après son déplacement, il revient souvent à sa position comme il est censé le faire, mais souvent pas. Il faut généralement quelques tentatives pour que cela se produise. Des valeurs impaires pour le temps (très faible) et/ou la distance (plutôt élevée) apparaissent au déclenchement, ce qui entraîne une vitesse élevée alors que le scrollView est, en réalité, presque ou totalement stationnaire . Lorsque l'utilisateur appuie sur le défilement pour arrêter son mouvement, il semble que le défilement règle la variable contentOffset à son emplacement précédent. Cette téléportation se traduit par une valeur de vitesse très élevée. Cela pourrait être corrigé en vérifiant si le delta précédent est currentDelta * -1, mais je préférerais une solution plus stable.

J'ai essayé d'utiliser didEndDecelerating, mais lorsque le problème survient, il n'est pas appelé. Cela confirme probablement que c'est déjà stationnaire. Il semble n'y avoir aucune méthode déléguée appelée lorsque la vue de défilement a cessé de se déplacer complètement.

Si vous souhaitez voir le problème vous-même, voici un code pour remplir le scrollview avec des onglets:

@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

Une version plus courte de cette question: comment savoir quand le scrollview a cessé de défiler? didEndDecelerating: n'est pas appelé lorsque vous le relâchez, didEndDragging: se passe souvent pendant le défilement et la vérification de la vitesse n'est pas fiable en raison de cet étrange «saut» qui définit la vitesse de manière aléatoire.

25
Aberrant

J'ai trouvé une solution:

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

Je n'avais pas remarqué ce dernier bit auparavant, willDecelerate. Il est faux lorsque scrollView est immobile lors de la fin du toucher. Combiné au contrôle de vitesse mentionné ci-dessus, je peux prendre une photo à la fois quand il est lent (et non touché) ou quand il est immobile.

Pour ceux qui ne font pas de capture mais qui doivent savoir quand le défilement est arrêté, didEndDecelerating sera appelé à la fin du mouvement de défilement. Combiné à une vérification de willDecelerate dans didEndDragging, vous saurez quand le défilement s'est arrêté.

30
Aberrant

[Réponse modifiée] C'est ce que j'utilise - il gère tous les cas Edge. Vous avez besoin d'un ivar pour conserver cet état et, comme indiqué dans les commentaires, il existe d'autres moyens de gérer cela.

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

J'ai créé un projet très simple qui utilise le code ci-dessus. Ainsi, lorsqu'une personne interagit avec un scrollView (y compris un WebView), elle empêche tout travail intensif en processus jusqu'à ce que l'utilisateur cesse d'interagir avec scrollView ET que le scrollview cesse de défiler. C'est comme 50 lignes de code: ScrollWatcher

21
David H

Voici comment combiner scrollViewDidEndDecelerating et scrollViewDidEndDragging:willDecelerate pour effectuer une opération lorsque le défilement est terminé:

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

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

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

Les méthodes de délégation mentionnées dans cet article ne m'ont pas aidé. J'ai trouvé une autre réponse qui détecte la fin du défilement finale dans scrollViewDidScroll: Le lien est ici

2
NightFury

J'ai répondu à cette question sur mon blog qui explique comment le faire sans problèmes.

Cela implique d'intercepter le délégué pour faire quelques choses, puis de transmettre les messages du délégué là où ils étaient destinés. Cela est nécessaire car il existe quelques scénarios dans lesquels le délégué est déclenché s'il est déplacé par programme, ou les deux à la fois, et de toute façon, il est préférable de consulter simplement le lien ci-dessous:

Comment savoir quand UIScrollView défile

C'est un peu délicat, mais j'ai préparé une recette et expliqué les parties en détail.

1
horseshoe7