我必须说,你的处境非常艰难。
请注意,您需要使用 UIScrollView withpagingEnabled=YES
在页面之间切换,但您需要pagingEnabled=NO
垂直滚动。
有两种可能的策略。我不知道哪一个会起作用/更容易实现,所以两个都试试。
第一:嵌套的 UIScrollViews。坦率地说,我还没有看到有人让这个工作。但是我个人并没有足够努力,我的实践表明,当你足够努力时,你可以让 UIScrollView 做任何你想做的事情。
所以策略是让外层滚动视图只处理水平滚动,而内层滚动视图只处理垂直滚动。要做到这一点,您必须知道 UIScrollView 在内部是如何工作的。它覆盖hitTest
方法并始终返回自身,以便所有触摸事件都进入 UIScrollView。然后在里面touchesBegan
,touchesMoved
等它检查它是否对事件感兴趣,然后处理或传递给内部组件。
为了决定触摸是被处理还是被转发,当你第一次触摸它时 UIScrollView 会启动一个计时器:
如果您在 150 毫秒内没有显着移动手指,它会将事件传递到内部视图。
如果您在 150 毫秒内显着移动了手指,它就会开始滚动(并且永远不会将事件传递到内部视图)。
请注意,当您触摸表格(它是滚动视图的子类)并立即开始滚动时,您所触摸的行永远不会突出显示。
如果您在 150 毫秒内没有显着移动手指并且 UIScrollView 开始将事件传递给内部视图,但是您已经将手指移动得足够远以开始滚动,则 UIScrollView 调用touchesCancelled
内部视图并开始滚动。
请注意,当您触摸表格时,请稍稍按住手指然后开始滚动,您触摸的行首先突出显示,但随后取消突出显示。
这些事件的顺序可以通过 UIScrollView 的配置来改变:
- 如果
delaysContentTouches
为 NO,则不使用计时器 - 事件立即转到内部控件(但如果您将手指移动得足够远,则会取消)
- 如果
cancelsTouches
为 NO,则一旦将事件发送到控件,将永远不会发生滚动。
请注意,它是 UIScrollView 从 CocoaTouch 接收所有 touchesBegin
、touchesMoved
和事件touchesEnded
(touchesCanceled
因为它hitTest
告诉它这样做)。然后,只要它愿意,它就会将它们转发到内部视图。
现在您已经了解了有关 UIScrollView 的所有信息,您可以更改它的行为。我敢打赌你想优先考虑垂直滚动,这样一旦用户触摸视图并开始移动他的手指(即使是轻微的),视图就会开始在垂直方向滚动;但是当用户在水平方向上移动手指足够远时,您想取消垂直滚动并开始水平滚动。
您希望将外部 UIScrollView 子类化(例如,您将类命名为 RemorsefulScrollView),以便它立即将所有事件转发到内部视图,而不是默认行为,并且只有在检测到显着水平移动时才会滚动。
如何使 RemorsefulScrollView 表现得那样?
看起来禁用垂直滚动并设置delaysContentTouches
为 NO 应该使嵌套的 UIScrollViews 工作。不幸的是,它没有;UIScrollView 似乎对快速运动(无法禁用)进行了一些额外的过滤,因此即使 UIScrollView 只能水平滚动,它总是会吃掉(并忽略)足够快的垂直运动。
效果非常严重,以至于嵌套滚动视图内的垂直滚动无法使用。(看来您已经完全掌握了这个设置,所以试试吧:按住手指 150 毫秒,然后垂直移动它——嵌套的 UIScrollView 会按预期工作!)
这意味着您不能使用 UIScrollView 的代码进行事件处理;您必须覆盖 RemorsefulScrollView 中的所有四种触摸处理方法并首先进行自己的处理,super
如果您决定使用水平滚动,则仅将事件转发到 (UIScrollView)。
但是,您必须传递touchesBegan
给 UIScrollView,因为您希望它记住未来水平滚动的基本坐标(如果您以后确定它是水平滚动)。稍后您将无法发送touchesBegan
到 UIScrollView,因为您无法存储touches
参数:它包含将在下一个touchesMoved
事件之前发生变异的对象,并且您无法重现旧状态。
所以你必须立即传递touchesBegan
给 UIScrollView,但是你会隐藏任何进一步touchesMoved
的事件,直到你决定水平滚动。NotouchesMoved
表示不滚动,所以这个首字母touchesBegan
不会造成任何伤害。但是一定要设置delaysContentTouches
为 NO,这样不会有额外的惊喜计时器干扰。
(题外话——与你不同,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
我没有尝试运行甚至编译它(并在纯文本编辑器中输入了整个类),但是您可以从上面的内容开始,并希望它能够正常工作。
我看到的唯一隐藏的问题是,如果您将任何非 UIScrollView 子视图添加到 RemorsefulScrollView,如果孩子并不总是像 UIScrollView 那样处理触摸,您转发给孩子的触摸事件可能会通过响应链返回给您。一个防弹的 RemorsefulScrollView 实现可以防止touchesXxx
重新进入。
第二种策略:如果由于某种原因嵌套的 UIScrollViews 无法正常工作或证明太难以正确,您可以尝试只使用一个 UIScrollView,pagingEnabled
从您的scrollViewDidScroll
委托方法动态切换其属性。
为了防止对角滚动,您应该首先尝试记住 contentOffset ,如果检测到对角移动,请scrollViewWillBeginDragging
检查并重置里面的 contentOffset 。scrollViewDidScroll
另一种尝试的策略是将 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 README。