ios 如何确切知道 UIScrollView 的滚动何时停止?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/7675497/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to know exactly when a UIScrollView's scrolling has stopped?
提问by Aberrant
In short, I need to know exactly when the scrollview stopped scrolling. By 'stopped scrolling', I mean the moment at which it is no longer moving and not being touched.
简而言之,我需要确切地知道滚动视图何时停止滚动。通过“停止滚动”,我的意思是它不再移动且未被触摸的那一刻。
I've been working on a horizontal UIScrollView subclass (for iOS 4) with selection tabs in it. One of its requirements is that it stops scrolling below a certain speed to allow user interaction more quickly. It should also snap to the start of a tab. In other words, when the user releases the scrollview and its speed is low, it snaps to a position. I've implemented this and it works, but there's a bug in it.
我一直在研究带有选择选项卡的水平 UIScrollView 子类(适用于 iOS 4)。它的一个要求是它停止滚动低于特定速度以允许用户更快地交互。它还应该捕捉到选项卡的开头。换句话说,当用户释放滚动视图并且它的速度很低时,它会捕捉到一个位置。我已经实现了这个并且它有效,但是它有一个错误。
What I have now:
我现在所拥有的:
The scrollview is its own delegate. at every call to scrollViewDidScroll:, it refreshes its speed-related variables:
滚动视图是它自己的委托。在每次调用 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);
}
Then proceeds to snap if needed:
如果需要,然后继续捕捉:
-(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;
}
Here are the delegate methods:
以下是委托方法:
-(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];
}
This works well most of the time, except in two scenarios:
1. When the user scrolls without releasing his/her finger and lets go at a near stationary timing right after moving, it often snaps to its position as it's supposed to, but a lot of times, does not. It usually takes a few attempts to get it to happen. Odd values for time (very low) and/or distance (rather high) appear at the release, causing a high speed value while the scrollView is, in reality, nearly or entirely stationary.
2. When the user taps the scrollview to stop its movement, it seems the scrollview sets the contentOffset
to its previous spot. This teleportation results in a very high speed value. This could be fixed by checking if the previous delta is currentDelta*-1, but I'd prefer a more stable solution.
这在大多数情况下效果很好,除了两种情况: 1. 当用户在不松开手指的情况下滚动并在移动后立即在接近静止的时间放开时,它通常会像预期的那样捕捉到它的位置,但是很多时候,没有。通常需要几次尝试才能实现。时间(非常低)和/或距离(相当高)的奇数值出现在发布时,导致滚动视图实际上几乎或完全静止时的高速值。2. 当用户点击滚动视图以停止其移动时,滚动视图似乎将 设置contentOffset
为之前的位置。这种传送会产生非常高的速度值。这可以通过检查前一个增量是否为 currentDelta*-1 来解决,但我更喜欢更稳定的解决方案。
I've tried using didEndDecelerating
, but when the glitch occurs, it does not get called. This probably confirms that it's stationary already. There seems to be no delegate method that gets called when the scrollview stopped moving completely.
我试过使用didEndDecelerating
,但是当出现故障时,它不会被调用。这可能证实它已经是静止的。当滚动视图完全停止移动时,似乎没有调用委托方法。
If you'd like to see the glitch yourself, here's some code to fill the scrollview with tabs:
如果您想自己查看故障,这里有一些代码可以用选项卡填充滚动视图:
@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
A shorter version of this question: how to know when the scrollview stopped scrolling? didEndDecelerating:
does not get called when you release it stationary, didEndDragging:
happens a lot during the scrolling and checking for speed is unreliable due to this odd 'jump' which sets the speed to something random.
这个问题的简短版本:如何知道滚动视图何时停止滚动?didEndDecelerating:
当您释放它静止不动时不会被调用,didEndDragging:
在滚动过程中发生了很多事情,并且由于这种奇怪的“跳跃”将速度设置为随机值,因此检查速度是不可靠的。
回答by Aberrant
I found a solution:
我找到了一个解决方案:
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
I did not notice that last bit before, willDecelerate
. It is false when the scrollView is stationary when ending the touch. Combined with the above-mentioned speed check, I can snap both when it's slow (and not being touched) or when it's stationary.
我之前没有注意到最后一点,willDecelerate
。当滚动视图在结束触摸时静止时为假。结合上述速度检查,我可以在它很慢(并且没有被触摸)或静止时捕捉。
For anyone not doing any snapping but needs to know when scrolling stopped, didEndDecelerating
will be called at the end of the scroll movement. Combined with a check on willDecelerate
in didEndDragging
, you'll know when the scrolling has stopped.
对于不进行任何捕捉但需要知道滚动何时停止的任何人,didEndDecelerating
将在滚动运动结束时调用。结合 check willDecelerate
in didEndDragging
,您将知道滚动何时停止。
回答by David H
[Edited Answer] This is what I use - it handles all the edge cases. You need an ivar to keep state, and as shown in the comments, there are other ways to handle this.
[编辑的答案] 这就是我使用的 - 它处理所有边缘情况。您需要一个 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");
}
I created a really simple project uses the above code, so that when a person interacts with a scrollView (including a WebView), it inhibits process intensive work until the user stops interacting with the scrollView AND the scrollview has stopped scrolling. It's like 50 lines of code: ScrollWatcher
我使用上面的代码创建了一个非常简单的项目,这样当一个人与一个滚动视图(包括一个 WebView)交互时,它会禁止处理密集型工作,直到用户停止与滚动视图交互并且滚动视图停止滚动。就像 50 行代码:ScrollWatcher
回答by Wayne
Here's how to combine scrollViewDidEndDecelerating
and scrollViewDidEndDragging:willDecelerate
to perform some operation when scrolling has finished:
以下是滚动完成后如何组合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
}
回答by NightFury
回答by horseshoe7
I answered this question on my blog which outlines how to do it without problems.
我在我的博客上回答了这个问题,其中概述了如何毫无问题地做到这一点。
It involves 'intercepting the delegate' to do a few things, then to pass the delegate messages to where they were intended. This is required because there are a few scenarios where the delegate fires if it moved programmatically, or not, or both, and so anyway, best to just look at this link below:
它涉及“拦截委托”来做一些事情,然后将委托消息传递到他们想要的地方。这是必需的,因为在某些情况下,如果委托以编程方式移动或不以编程方式移动,或两者兼而有之,则会触发委托,因此无论如何,最好只查看下面的链接:
How to know when a UIScrollView is scrolling
It's a little tricky, but I've put up a recipe there and have explained the parts in detail.
这有点棘手,但我已经在那里提出了一个食谱并详细解释了各个部分。
回答by LukeSideWalker
It is important to understand, that the when UIScrollView is stopping to move it triggers two different functionsof the UIScrollViewDelegate depending on how strong the user has pushed.
重要的是要理解,当 UIScrollView 停止移动时,它会根据用户推动的强度触发UIScrollViewDelegate 的两个不同功能。
- stopDraggingis triggered, when the user stops his drag gesture on the screen. When this was very slowly no more movement will happen after that.
- stopDeceleratingis triggered, when the user has pushedthe scroll view in a way, so that it is still moving after the user touches up his finger and then comes to an end. This function is not always triggered, it is not triggered, when there is no movement after the user touches up his finger. Then only the function above "stopDragging" is triggered.
- 当用户在屏幕上停止拖动手势时,会触发stopDragging。当这非常缓慢时,之后就不会再发生任何运动。
- stopDecelerating被触发,当用户以某种方式推动滚动视图时,以便用户触摸他的手指后它仍然在移动,然后结束。此功能并不总是触发,它不会在用户触摸手指后没有移动时触发。然后只触发“stopDragging”上面的函数。
So in conclusion it is necessary to combine the two functionslike @WayneBurkett already pointed out in his answer.
因此,总而言之,有必要将这两个功能结合起来,就像@WayneBurkett 在他的回答中已经指出的那样。
In Swift 5.X:
在 Swift 5.X 中:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
myFunction()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if decelerate == false {
myFunction()
}
}
private func myFunction() {
// scrollView did stop moving -> do what ever
// ...
}