web-dev-qa-db-ja.com

UIScrollViewのスクロールがいつ停止したかを正確に知る方法は?

要するに、私はスクロールビューがスクロールを停止したときを正確に知る必要があります。 「スクロールの停止」とは、スクロールが動かなくなり、触れられない瞬間を意味します。

私は、選択タブを含む水平UIScrollViewサブクラス(iOS 4用)に取り組んでいます。その要件の1つは、ユーザーの操作をより迅速に行えるように、特定の速度以下でスクロールを停止することです。また、タブの先頭にスナップする必要があります。言い換えれば、ユーザーがスクロールビューをリリースし、その速度が遅い場合、それは位置にスナップします。これを実装しましたが機能しますが、バグがあります。

私が今持っているもの:

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

次の2つのシナリオを除き、これはほとんどの場合にうまく機能します。多くの場合、そうではありません。通常、それを実現するには数回の試行が必要です。リリースでは、時間(非常に低い)および/または距離(やや高い)の奇数値が表示され、実際にはscrollViewがほぼまたは完全に静止している間に高速値が発生します。 2.ユーザーがスクロールビューをタップして移動を停止すると、スクロールビューがcontentOffsetを前のスポットに設定したようです。このテレポーテーションは、非常に高い速度値をもたらします。これは、前のデルタがcurrentDelta * -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

この質問の短縮版:scrollviewがスクロールを停止した時期を知る方法は? didEndDecelerating:は、静止したときに呼び出されません、didEndDragging:はスクロール中に頻繁に発生し、速度をランダムに設定するこの奇妙な「ジャンプ」のため、速度のチェックは信頼できません。

26
Aberrant

私は解決策を見つけました:

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

その最後のビットwillDecelerateに気が付きませんでした。タッチを終了するときにscrollViewが静止している場合はfalseです。上記の速度チェックと組み合わせて、遅い(触れられていない)ときと静止しているときの両方をスナップできます。

スナップを行っていないが、スクロールがいつ停止したかを知る必要がある場合は、didEndDeceleratingがスクロール移動の最後に呼び出されます。 willDeceleratedidEndDraggingのチェックと組み合わせると、スクロールがいつ停止したかがわかります。

30
Aberrant

[編集済み回答]これは私が使用するものです-それはすべてのエッジのケースを処理します。状態を維持するにはivarが必要です。コメントに示されているように、これを処理する方法は他にもあります。

- (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との対話を停止し、scrollviewがスクロールを停止するまで、プロセスの集中的な作業を禁止します。 50行のコードのようなものです。 ScrollWatcher

21
David H

scrollViewDidEndDeceleratingscrollViewDidEndDragging:willDecelerateを組み合わせて、スクロールが終了したときに何らかの操作を実行する方法は次のとおりです。

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

この投稿の回答に記載されているデリゲートメソッドは役に立ちませんでした。 scrollViewDidScroll:リンクの最終スクロール終了を検出する別の答えが見つかりました- ここ

2
NightFury

私はブログでこの問題に答え、問題なくそれを行う方法の概要を説明しました。

いくつかのことを行うために「デリゲートをインターセプト」し、デリゲートメッセージを目的の場所に渡します。これが必要なのは、デリゲートがプログラムで移動した場合、または移動しなかった場合、またはその両方の場合にデリゲートが発動するシナリオがいくつかあるためです。

IScrollViewがいつスクロールするかを知る方法

少し注意が必要ですが、そこにレシピを掲載し、部品について詳しく説明しました。

1
horseshoe7