如何强制进行UIScrollView
分页和滚动仅在给定时刻垂直或水平移动?
我的理解是directionalLockEnabled
属性应该实现这一点,但是对角线滑动仍然导致视图对角滚动而不是将运动限制到单个轴.
编辑:为了更清楚,我想让用户水平或垂直滚动,但不能同时滚动.
我必须说,你处境非常艰难.
请注意,您需要使用UIScrollView pagingEnabled=YES
来切换页面,但需要pagingEnabled=NO
垂直滚动.
有两种可能的策略.我不知道哪一个会起作用/更容易实现,所以请尝试两者.
第一:嵌套的UIScrollViews.坦率地说,我还没有看到一个人有这个工作.但是我个人并没有努力,我的练习表明,当你努力尝试时,你可以让UIScrollView做你想做的任何事情.
因此策略是让外部滚动视图仅处理水平滚动,而内部滚动视图仅处理垂直滚动.要实现这一点,您必须知道UIScrollView如何在内部工作.它会覆盖hitTest
方法并始终返回自身,以便所有触摸事件都进入UIScrollView.然后里面touchesBegan
,touchesMoved
等它检查是否是感兴趣的事件,并且会处理或将其传递到内部组件.
要决定是要处理还是要转发触摸,UIScrollView会在您第一次触摸时启动计时器:
如果您未在150毫秒内显着移动手指,则会将事件传递到内部视图.
如果您在150毫秒内显着移动手指,它将开始滚动(并且永远不会将事件传递到内部视图).
请注意当您触摸表格(滚动视图的子类)并立即开始滚动时,您触摸的行永远不会突出显示.
如果你还没有内150毫秒显著移动你的手指的UIScrollView开始传递事件内视图,但随后您移动手指远远不够的滚动开始,UIScrollView的要求touchesCancelled
在内部视图和开始滚动.
请注意当您触摸桌子时,稍微握住手指然后开始滚动,您首先突出显示您触摸的行,但之后不再突出显示.
可以通过配置UIScrollView来更改这些事件序列:
如果delaysContentTouches
为NO,则不使用计时器 - 事件立即进入内部控制(但如果您将手指移得足够远则取消)
如果cancelsTouches
为NO,则一旦将事件发送到控件,滚动将永远不会发生.
需要注意的是的UIScrollView接收所有 touchesBegin
,touchesMoved
,touchesEnded
和touchesCanceled
从CocoaTouch事件(因为它hitTest
告诉它这样做).然后,只要它想要,它就会将它们转发到内部视图.
现在您已了解UIScrollView的所有内容,您可以更改其行为.我敢打赌你想优先选择垂直滚动,这样一旦用户触摸视图并开始移动他的手指(甚至略微),视图就会开始在垂直方向滚动; 但是当用户将手指在水平方向上移动足够远时,您想要取消垂直滚动并开始水平滚动.
你想你的子类外的UIScrollView(比方说,你的名字你的类RemorsefulScrollView),以便而不是默认的行为立即转发所有事件到内查看,并检测显著水平运动,只有当它滚动.
如何让RemorsefulScrollView表现那样?
看起来禁用垂直滚动并设置delaysContentTouches
为NO应该使嵌套的UIScrollViews工作.不幸的是,它没有; 的UIScrollView似乎做快速运动(不能被禁用)一些额外的过滤,这样即使UIScrollView的只能水平滚动,它总是会吃掉(和忽略)足够快的垂直运动.
效果非常严重,嵌套滚动视图内的垂直滚动无法使用.(看起来你已经完成了这个设置,所以试试吧:握住手指150ms,然后沿垂直方向移动 - 嵌套的UIScrollView按预期工作!)
这意味着您无法使用UIScrollView的代码进行事件处理; 您必须覆盖RemorsefulScrollView中的所有四种触摸处理方法并首先执行您自己的处理,super
如果您决定使用水平滚动,则只将事件转发到(UIScrollView).
但是,您必须传递touchesBegan
给UIScrollView,因为您希望它记住未来水平滚动的基本坐标(如果您稍后决定它是水平滚动).您将无法touchesBegan
稍后发送到UIScrollView,因为您无法存储touches
参数:它包含将在下一个touchesMoved
事件之前发生变化的对象,并且您无法重现旧状态.
因此,您必须touchesBegan
立即传递给UIScrollView,但touchesMoved
在您决定水平滚动之前,您将隐藏其中的任何其他事件.没touchesMoved
意味着没有滚动,所以这个首字母touchesBegan
不会有任何伤害.但是设置delaysContentTouches
为NO,这样就不会有额外的惊喜计时器干扰.
(Offtopic - 与您不同,UIScrollView 可以正确存储触摸并可以在touchesBegan
以后重现和转发原始事件.它具有使用未发布的API的不公平优势,因此可以在变异之前克隆触摸对象.)
鉴于你总是前进touchesBegan
,你也必须前进touchesCancelled
和touchesEnded
.你必须把touchesEnded
成touchesCancelled
,但是,因为UIScrollView中会解释touchesBegan
,touchesEnded
序列为触摸点击,和将其转发到内部视图.您已经自己转发了正确的事件,因此您永远不希望UIScrollView转发任何内容.
基本上这里是你需要做的伪代码.为简单起见,我在多点触控事件发生后从不允许水平滚动.
// RemorsefulScrollView.h @interface RemorsefulScrollView : UIScrollView { CGPoint _originalPoint; BOOL _isHorizontalScroll, _isMultitouch; UIView *_currentChild; } @end // RemorsefulScrollView.m // the numbers from an example in Apple docs, may need to tune them #define kThresholdX 12.0f #define kThresholdY 4.0f @implementation RemorsefulScrollView - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.delaysContentTouches = NO; } return self; } - (id)initWithCoder:(NSCoder *)coder { if (self = [super initWithCoder:coder]) { self.delaysContentTouches = NO; } return self; } - (UIView *)honestHitTest:(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; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; // always forward touchesBegan -- there's no way to forward it later if (_isHorizontalScroll) return; // UIScrollView is in charge now if ([touches count] == [[event touchesForView:self] count]) { // initial touch _originalPoint = [[touches anyObject] locationInView:self]; _currentChild = [self honestHitTest:_originalPoint withEvent:event]; _isMultitouch = NO; } _isMultitouch |= ([[event touchesForView:self] count] > 1); [_currentChild touchesBegan:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { if (!_isHorizontalScroll && !_isMultitouch) { CGPoint point = [[touches anyObject] locationInView:self]; if (fabsf(_originalPoint.x - point.x) > kThresholdX && fabsf(_originalPoint.y - point.y) < kThresholdY) { _isHorizontalScroll = YES; [_currentChild touchesCancelled:[event touchesForView:self] withEvent:event] } } if (_isHorizontalScroll) [super touchesMoved:touches withEvent:event]; // UIScrollView only kicks in on horizontal scroll else [_currentChild touchesMoved:touches withEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (_isHorizontalScroll) [super touchesEnded:touches withEvent:event]; else { [super touchesCancelled:touches withEvent:event]; [_currentChild touchesEnded:touches withEvent:event]; } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; if (!_isHorizontalScroll) [_currentChild touchesCancelled:touches withEvent:event]; } @end
我没有尝试运行甚至编译它(并在纯文本编辑器中键入整个类),但是你可以从上面开始并希望它能够正常工作.
我看到的唯一隐藏的问题是,如果你向RemorsefulScrollView添加任何非UIScrollView子视图,你转发给孩子的触摸事件可能会通过响应者链返回给你,如果孩子并不总是像UIScrollView那样处理触摸.一个防弹的RemorsefulScrollView实现可以防止touchesXxx
重新进入.
第二种策略:如果由于某种原因嵌套的UIScrollViews无法解决或证明难以正确使用,您可以尝试与一个UIScrollView相处,pagingEnabled
从您的scrollViewDidScroll
委托方法动态切换其属性.
要防止对角线滚动,您应首先尝试记住contentOffset scrollViewWillBeginDragging
,并在scrollViewDidScroll
检测到对角线移动时检查并重置内部的contentOffset .另一种尝试的策略是将contentSize重置为仅在一个方向上滚动,一旦您确定了用户手指的方向.(UIScrollView似乎非常宽容从其委托方法中摆弄contentSize和contentOffset.)
如果不工作,要么或导致马虎的视觉效果,你必须重写touchesBegan
,touchesMoved
等等,而不是向对角线移动事件的UIScrollView.(在这种情况下,用户体验不是最理想的,因为你必须忽略对角线的移动,而不是强迫它们朝一个方向.如果你真的有冒险精神,你可以编写自己的UITouch,就像RevengeTouch一样. -C是普通的老C,世界上没有什么比C更好的了;只要没有人检查对象的真实类,我认为没有人做,你可以使任何类看起来像任何其他类.这打开了可以使用您想要的任何坐标来合成您想要的任何触摸.)
备份策略:有TTScrollView,一个在Three20库中的UIScrollView的理智重新实现.不幸的是,它对用户来说感觉非常不自然和非iphonish.但是,如果每次使用UIScrollView的尝试都失败,您可以回退到自定义编码的滚动视图.如果可能的话我建议反对它; 使用UIScrollView可确保您获得原生外观,无论它在未来的iPhone OS版本中如何发展.
好的,这篇小文章有点太长了.几天前我在ScrollingMadness上工作后,我还在进入UIScrollView游戏.
PS如果你有任何这些工作并且想分享,请发送电子邮件给我在andreyvit@gmail.com的相关代码,我很乐意将它包含在我的ScrollingMadness包中.
PPS将这篇小文章添加到ScrollingMadness自述文件中.
这对我每次都有用......
scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);
对于像我这样懒惰的人:
设置scrollview.directionalLockEnabled
为YES
- (void) scrollViewWillBeginDragging: (UIScrollView *) scrollView { self.oldContentOffset = scrollView.contentOffset; } - (void) scrollViewDidScroll: (UIScrollView *) scrollView { if (scrollView.contentOffset.x != self.oldContentOffset.x) { scrollView.pagingEnabled = YES; scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, self.oldContentOffset.y); } else { scrollView.pagingEnabled = NO; } } - (void) scrollViewDidEndDecelerating: (UIScrollView *) scrollView { self.oldContentOffset = scrollView.contentOffset; }
我用以下方法来解决这个问题,希望对你有帮助.
首先在控制器头文件中定义以下变量.
CGPoint startPos; int scrollDirection;
当您的委托收到scrollViewWillBeginDragging消息时,startPos将保留contentOffset值.所以在这种方法中我们这样做;
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{ startPos = scrollView.contentOffset; scrollDirection=0; }
然后我们使用这些值来确定用户在scrollViewDidScroll消息中的预期滚动方向.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{ if (scrollDirection==0){//we need to determine direction //use the difference between positions to determine the direction. if (abs(startPos.x-scrollView.contentOffset.x)最后我们必须在用户结束拖动时停止所有更新操作;
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ if (decelerate) { scrollDirection=3; } }
5> James J..:我可能会在这里进行重击,但今天我偶然发现了这篇文章,因为我试图解决同样的问题.这是我的解决方案,似乎工作得很好.
我想显示一些屏幕内容,但允许用户滚动到其他4个屏幕中的一个,向上和向下.我有一个大小为320x480的UIScrollView和一个960x1440的contentSize.内容偏移量从320,480开始.在scrollViewDidEndDecelerating:中,我重绘了5个视图(中心视图和它周围的4个视图),并将内容偏移重置为320,480.
这是我的解决方案的主要内容,即scrollViewDidScroll:方法.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (!scrollingVertically && ! scrollingHorizontally) { int xDiff = abs(scrollView.contentOffset.x - 320); int yDiff = abs(scrollView.contentOffset.y - 480); if (xDiff > yDiff) { scrollingHorizontally = YES; } else if (xDiff < yDiff) { scrollingVertically = YES; } } if (scrollingHorizontally) { scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, 480); } else if (scrollingVertically) { scrollView.contentOffset = CGPointMake(320, scrollView.contentOffset.y); } }