我必须说,你的处境非常艰难。
请注意,您需要使用带有pagingEnabled=YES 的 UIScrollView 在页面之间切换,但您需要使用pagingEnabled=NO 来垂直滚动。
有两种可能的策略。我不知道哪一个会起作用/更容易实现,所以两个都试试。
第一: 嵌套的 UIScrollViews。坦率地说,我还没有看到有人让这个工作。不过我个人还不够努力,我的实践证明,当你够努力的时候,你可以make UIScrollView do anything you want。
所以策略是让外层滚动视图只处理水平滚动,而内层滚动视图只处理垂直滚动。要做到这一点,您必须知道 UIScrollView 在内部是如何工作的。它覆盖hitTest 方法并始终返回自身,以便所有触摸事件都进入UIScrollView。然后在touchesBegan、touchesMoved 等内部检查它是否对事件感兴趣,然后处理或将其传递给内部组件。
为了决定触摸是被处理还是被转发,UIScrollView 在你第一次触摸它时启动一个计时器:
如果您的手指在 150 毫秒内没有明显移动,它会将事件传递到内部视图。
-
如果您在 150 毫秒内显着移动了手指,它就会开始滚动(并且永远不会将事件传递到内部视图)。
请注意,当您触摸表格(它是滚动视图的子类)并立即开始滚动时,您所触摸的行永远不会突出显示。
-
如果您没有在 150 毫秒内显着移动手指并且 UIScrollView 开始将事件传递给内部视图,但是 那么您已经将手指移动得足够远以进行滚动首先,UIScrollView 在内部视图上调用 touchesCancelled 并开始滚动。
请注意,当您触摸表格时,稍微按住手指然后开始滚动,您触摸的行首先突出显示,然后取消突出显示。
这些事件的顺序可以通过 UIScrollView 的配置来改变:
- 如果
delaysContentTouches 为 NO,则不使用计时器 - 事件立即转到内部控件(但如果您将手指移动得足够远,则会取消)
- 如果
cancelsTouches 为NO,则一旦将事件发送到控件,将永远不会发生滚动。
请注意,UIScrollView 从 CocoaTouch 接收 all 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 事件,直到你决定水平滚动。没有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
我没有尝试运行甚至编译它(并在纯文本编辑器中输入了整个类),但您可以从上面开始,希望它能够正常工作。
我看到的唯一隐藏的问题是,如果您将任何非 UIScrollView 子视图添加到 RemorsefulScrollView,那么您转发给孩子的触摸事件可能会通过响应者链返回给您,如果孩子并不总是处理像 UIScrollView 这样的触摸做。一个防弹的 RemorsefulScrollView 实现可以防止 touchesXxx 重入。
第二种策略:如果由于某种原因嵌套的 UIScrollViews 不能正常工作或证明太难正确,您可以尝试只使用一个 UIScrollView,将其 pagingEnabled 属性打开从你的 scrollViewDidScroll 委托方法飞。
为了防止对角滚动,您应该首先尝试记住 scrollViewWillBeginDragging 中的 contentOffset,如果检测到对角移动,请检查并重置 scrollViewDidScroll 中的 contentOffset。另一种尝试的策略是将 contentSize 重置为仅在一个方向上启用滚动,一旦您决定用户手指的方向。 (UIScrollView 似乎很宽容地从它的委托方法中摆弄 contentSize 和 contentOffset。)
如果这也不起作用或导致视觉效果草率,您必须覆盖touchesBegan、touchesMoved 等,而不是将对角线移动事件转发到 UIScrollView。 (但是,在这种情况下,用户体验将不是最理想的,因为您将不得不忽略对角线的移动,而不是强迫它们向一个方向移动。如果您真的很喜欢冒险,您可以编写自己的 UITouch 类似的东西,比如 RevengeTouch。目标-C 是普通的旧 C,世界上没有比 C 更鸭式的了;只要没有人检查对象的真实类,我相信没有人这样做,你可以让任何类看起来像任何其他类。这打开了可以用你想要的任何坐标来合成你想要的任何触摸。)
备份策略: TTScrollView 是 Three20 library 中 UIScrollView 的合理重新实现。不幸的是,它对用户来说感觉非常不自然和非 iphonish。但是如果使用 UIScrollView 的每一次尝试都失败了,你可以回退到自定义编码的滚动视图。如果可能的话,我建议不要这样做;使用 UIScrollView 可确保您获得本机外观,无论它在未来的 iPhone OS 版本中如何发展。
好的,这篇小论文有点太长了。几天前完成了 ScrollingMadness 的工作后,我仍然喜欢 UIScrollView 游戏。
附:如果你得到这些工作并想分享,请将相关代码通过电子邮件发送到 andreyvit@gmail.com,我很乐意将其包含在我的 ScrollingMadness bag of tricks 中。
P.P.S.将这篇小文章添加到 ScrollingMadness README。