UIScrollView:水平分页,垂直滚动? [英] UIScrollView : paging horizontally, scrolling vertically?

查看:115
本文介绍了UIScrollView:水平分页,垂直滚动?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何强制 UIScrollView ,其中分页和滚动仅在给定时刻垂直或水平移动?



我的理解是 directionalLockEnabled 属性应该实现这一点,但是对角线滑动仍会导致视图对角滚动而不是将运动限制为单个轴。 / p>

编辑:为了更清楚,我想允许用户水平或垂直滚动​​,但不能同时滚动。

解决方案

我必须说,你处境非常艰难。



注意你需要使用UIScrollView使用 pagingEnabled = YES 在页面之间切换,但需要 pagingEnabled = NO 才能垂直滚动。



有两种可能的策略。我不知道哪个会工作/更容易实现,所以请尝试两者。



第一个:嵌套的UIScrollViews。坦率地说,我还没有看到一个人有这个工作。但是我个人并没有努力,我的练习表明,当你努力尝试时,你可以让UIScrollView做你想要的任何东西



所以策略是让外部滚动视图只处理水平滚动,而内部滚动视图只处理垂直滚动。要实现这一点,您必须知道UIScrollView如何在内部工作。它会覆盖 hitTest 方法并始终返回自身,以便所有触摸事件都进入UIScrollView。然后在 touchesBegan touchesMoved 等内部检查它是否对该事件感兴趣,并将其处理或传递给内部组件。



要确定是要处理还是要转发触摸,UIScrollView会在您第一次触摸时启动计时器:




  • 如果你没有在150毫秒内显着移动手指,它会将事件传递到内部视图。


  • 如果你在150ms内显着移动手指,它会开始滚动(并且永远不会将事件传递到内部视图)。



    注意何时你触摸一个表(它是滚动视图的子类)并立即开始滚动,你触摸的行永远不会突出显示。


  • 如果你有在150ms内显着移动手指,UIScrollView开始将事件传递到内部视图,但是然后你已经将手指移动到足以让滚动开始,UIScrol lView在内部视图上调用 touchesCancelled 并开始滚动。



    注意当你触摸桌子时,握住你的手指一点然后开始滚动,你触摸的行首先突出显示,但之后不再突出显示。




这些可以通过配置UIScrollView来更改事件序列:




  • 如果 delaysContentTouches 为NO ,然后没有使用计时器 - 事件立即进入内部控制(但如果你的手指移动足够远,则取消)

  • 如果 cancelsTouches 为NO,然后一旦事件被发送到控件,滚动将永远不会发生。



请注意它是UIScrollView收到所有 touchesBegin touchesMoved touchesEnded touchesCanceled 事件(因为它的 hitTest 告诉它这样做)。然后,只要它想要,它就会将它们转发到内部视图。



现在您已经了解了有关UIScrollView的所有信息,您可以改变它的行为。我敢打赌你想优先考虑垂直滚动,这样一旦用户触摸视图并开始移动他的手指(甚至略微),视图就会开始在垂直方向上滚动;但是当用户将手指在水平方向上移动得足够远时,你想要取消垂直滚动并开始水平滚动。



你想要将你的外部UIScrollView子类化(比如,你将你的类命名为RemorsefulScrollView),这样它就可以立即将所有事件转发到内部视图,而不是默认行为,只有当检测到显着的水平移动时,它才会滚动。



要做RemorsefulScrollView的行为吗?




  • 看起来禁用垂直滚动并设置 delaysContentTouches to 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 _isHorizo​​ntalScroll,_isMultitouch;
UIView * _currentChild;
}
@end

// RemorsefulScrollView.m

//来自Apple文档示例中的数字,可能需要调整它们
#define kThresholdX 12.0f
#define kThresholdY 4.0f

@implementation RemorsefulScrollView

- (id)initWithFrame:(CGRect)frame {
if(self = [super initWithFrame:frame]){
self.delaysContentTouches = NO;
}
返回自我;
}

- (id)initWithCoder:(NSCoder *)编码器{
if(self = [super initWithCoder:coder]){
self.delaysContentTouches = NO ;
}
返回自我;
}

- (UIView *)honestHitTest:(CGPoint)指向withEvent:(UIEvent *)事件{
UIView * result = nil;
for(UIView * child in self.subviews)
if([child pointInside:point withEvent:event])
if((result = [child hitTest:point withEvent:event])! =零)
休息;
返回结果;
}

- (void)touchesBegan:(NSSet *)触及withEvent:(UIEvent *)事件{
[super touchesBegan:touches withEvent:event]; //总是转发touchesBegan - 以后无法转发
if(_isHorizo​​ntalScroll)
return; // UIScrollView现在负责
if([touches count] == [[event touchesForView:self] count]){//初始触摸
_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 *)触及withEvent:(UIEvent *)事件{
if(!_isHorizo​​ntalScroll&&!_isMultitouch){
CGPoint point = [[touches anyObject] locationInView:self];
if(fabsf(_originalPoint.x - point.x)> kThresholdX&& fabsf(_originalPoint.y - point.y)< kThresholdY){
_isHorizo​​ntalScroll = YES;
[_currentChild touchesCancelled:[event touchesForView:self] withEvent:event]
}
}
if(_isHorizo​​ntalScroll)
[super touchesMoved:touches withEvent:event]; // UIScrollView仅在水平滚动中启动
else
[_currentChild touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)触及withEvent:(UIEvent *)事件{
if(_isHorizo​​ntalScroll)
[super touchesEnded:touches withEvent :事件];
else {
[super touchesCancelled:touches withEvent:event];
[_currentChild touchesEnded:touches withEvent:event];
}
}

- (void)touchesCancelled:(NSSet *)触及withEvent:(UIEvent *)事件{
[super touchesCancelled:touches withEvent:event] ;
if(!_isHorizo​​ntalScroll)
[_currentChild touchesCancelled:touches withEvent:event];
}

@end

我还没试过去跑或者甚至编译它(并在纯文本编辑器中键入整个类),但是你可以从上面开始并希望它能正常工作。



唯一隐藏的捕获我看到,如果你向RemorsefulScrollView添加任何非UIScrollView子视图,你转发给孩子的触摸事件可能会通过响应者链返回给你,如果孩子并不总是像UIScrollView那样处理触摸。一个防弹的RemorsefulScrollView实现可以防止 touchesXxx 重新进入。



第二个策略:如果由于某种原因嵌套的UIScrollViews无法解决或证明难以正确使用,您可以尝试与一个UIScrollView相处,即时切换其 pagingEnabled 属性来自 scrollViewDidScroll 委托方法。



要防止对角线滚动,首先应该尝试在<$ c $中记住contentOffset c> scrollViewWillBeginDragging ,如果检测到对角线移动,则检查并重置 scrollViewDidScroll 内的contentOffset。另一种尝试的策略是将contentSize重置为仅在一个方向上滚动,一旦您确定了用户手指的方向。 (UIScrollView对于从其委托方法中摆弄contentSize和contentOffset似乎非常宽容。)



如果这不起作用或导致草率视觉效果,则必须覆盖 touchesBegan touchesMoved 等,而不是将对角线移动事件转发给UIScrollView。 (在这种情况下,用户体验不是最理想的,因为你必须忽略对角线的移动,而不是强迫它们朝向一个方向。如果你真的喜欢冒险,你可以编写自己的UITouch,就像RevengeTouch一样。 -C是普通的老C,世界上没有什么比C更好的了;只要没有人检查对象的真实类,我认为没有人做,你可以使任何类看起来像任何其他类。这打开了可以使用您想要的任何坐标来合成您想要的任何触摸。)



备份策略:有TTScrollView,UIScrollView的理智重新实现 Three20图书馆。不幸的是,它对用户来说感觉非常不自然和非iphonish。但是,如果每次使用UIScrollView的尝试都失败,您可以回退到自定义编码的滚动视图。如果可能的话,我建议不要这样做;使用UIScrollView可确保您获得原生的外观,无论它在未来的iPhone OS版本中如何发展。



好的,这篇小文章也有点过分了长。几天前我在ScrollingMadness上工作后,我仍然只是进入UIScrollView游戏。



P.S。如果你有任何这些工作并且想要分享,请发送电子邮件给我在andreyvit@gmail.com的相关代码,我很高兴将它包含在我的 ScrollingMadness魔术包



PPS将这篇小文章添加到ScrollingMadness README。


How can I force a UIScrollView in which paging and scrolling are on to only move vertically or horizontally at a given moment?

My understanding is that the directionalLockEnabled property should achieve this, but a diagonal swipe still causes the view to scroll diagonally instead of restricting motion to a single axis.

Edit: to be clearer, I'd like to allow the user to scroll horizontally OR vertically, but not both simultaneously.

解决方案

You're in a very tough situation, I must say.

Note that you need to use a UIScrollView with pagingEnabled=YES to switch between pages, but you need pagingEnabled=NO to scroll vertically.

There are 2 possible strategies. I don't know which one will work / is easier to implement, so try both.

First: nested UIScrollViews. Frankly, I'm yet to see a person who has got this to work. However I have not tried hard enough personally, and my practise shows that when you do try hard enough, you can make UIScrollView do anything you want.

So the strategy is to let the outer scroll view only handle horizontal scrolling, and inner scroll views to only handle vertical scrolling. To accomplish that, you must know how UIScrollView works internally. It overrides hitTest method and always returns itself, so that all touch events go into UIScrollView. Then inside touchesBegan, touchesMoved etc it checks if it's interested in the event, and either handles or passes it on to the inner components.

To decide if the touch is to be handled or to be forwarded, UIScrollView starts a timer when you first touch it:

  • If you haven't moved your finger significantly within 150ms, it passes the event on to the inner view.

  • If you have moved your finger significantly within 150ms, it starts scrolling (and never passes the event to the inner view).

    Note how when you touch a table (which is a subclass of scroll view) and start scrolling immediately, the row that you touched is never highlighted.

  • If you have not moved your finger significantly within 150ms and UIScrollView started passing the events to the inner view, but then you have moved the finger far enough for the scrolling to begin, UIScrollView calls touchesCancelled on the inner view and starts scrolling.

    Note how when you touch a table, hold your finger a bit and then start scrolling, the row that you touched is highlighted first, but de-highlighted afterwards.

These sequence of events can be altered by configuration of UIScrollView:

  • If delaysContentTouches is NO, then no timer is used — the events immediately go to the inner control (but then are canceled if you move your finger far enough)
  • If cancelsTouches is NO, then once the events are sent to a control, scrolling will never happen.

Note that it is UIScrollView that receives all touchesBegin, touchesMoved, touchesEnded and touchesCanceled events from CocoaTouch (because its hitTest tells it to do so). It then forwards them to the inner view if it wants to, as long as it wants to.

Now that you know everything about UIScrollView, you can alter its behavior. I can bet you want to give preference to vertical scrolling, so that once the user touches the view and starts moving his finger (even slightly), the view starts scrolling in vertical direction; but when the user moves his finger in horizontal direction far enough, you want to cancel vertical scrolling and start horizontal scrolling.

You want to subclass your outer UIScrollView (say, you name your class RemorsefulScrollView), so that instead of the default behaviour it immediately forwards all events to the inner view, and only when significant horizontal movement is detected it scrolls.

How to do make RemorsefulScrollView behave that way?

  • It looks like disabling vertical scrolling and setting delaysContentTouches to NO should make nested UIScrollViews to work. Unfortunately, it does not; UIScrollView appears to do some additional filtering for fast motions (which cannot be disabled), so that even if UIScrollView can only be scrolled horizontally, it will always eat up (and ignore) fast enough vertical motions.

    The effect is so severe that vertical scrolling inside a nested scroll view is unusable. (It appears that you have got exactly this setup, so try it: hold a finger for 150ms, and then move it in vertical direction — nested UIScrollView works as expected then!)

  • This means you cannot use UIScrollView's code for event handling; you have to override all four touch handling methods in RemorsefulScrollView and do your own processing first, only forwarding the event to super (UIScrollView) if you have decided to go with horizontal scrolling.

  • However you have to pass touchesBegan to UIScrollView, because you want it to remember a base coordinate for future horizontal scrolling (if you later decide it is a horizontal scrolling). You won't be able to send touchesBegan to UIScrollView later, because you cannot store the touches argument: it contains objects that will be mutated before the next touchesMoved event, and you cannot reproduce the old state.

    So you have to pass touchesBegan to UIScrollView immediately, but you will hide any further touchesMoved events from it until you decide to scroll horizontally. No touchesMoved means no scrolling, so this initial touchesBegan will do no harm. But do set delaysContentTouches to NO, so that no additional surprise timers interfere.

    (Offtopic — unlike you, UIScrollView can store touches properly and can reproduce and forward the original touchesBegan event later. It has an unfair advantage of using unpublished APIs, so can clone touch objects before they are mutated.)

  • Given that you always forward touchesBegan, you also have to forward touchesCancelled and touchesEnded. You have to turn touchesEnded into touchesCancelled, however, because UIScrollView would interpret touchesBegan, touchesEnded sequence as a touch-click, and would forward it to the inner view. You are already forwarding the proper events yourself, so you never want UIScrollView to forward anything.

Basically here's pseudocode for what you need to do. For simplicity, I never allow horizontal scrolling after multitouch event has occurred.

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

I have not tried to run or even to compile this (and typed the whole class in a plain text editor), but you can start with the above and hopefully get it working.

The only hidden catch I see is that if you add any non-UIScrollView child views to RemorsefulScrollView, the touch events you forward to a child may arrive back to you via responder chain, if the child does not always handle touches like UIScrollView does. A bullet-proof RemorsefulScrollView implementation would protect against touchesXxx reentry.

Second strategy: If due to some reason nested UIScrollViews do not work out or prove too hard to get right, you can try to get along with just one UIScrollView, switching its pagingEnabled property on the fly from your scrollViewDidScroll delegate method.

To prevent diagonal scrolling, you should first try remembering contentOffset in scrollViewWillBeginDragging, and checking and resetting contentOffset inside scrollViewDidScroll if you detect a diagonal movement. Another strategy to try is to reset contentSize to only enable scrolling in one direction, once you decide which direction the user's finger is going. (UIScrollView seems pretty forgiving about fiddling with contentSize and contentOffset from its delegate methods.)

If that does not work either or results in sloppy visuals, you have to override touchesBegan, touchesMoved etc and not forward diagonal movement events to UIScrollView. (The user experience will be suboptimal in this case however, because you will have to ignore diagonal movements instead of forcing them into a single direction. If you're feeling really adventurous, you can write your own UITouch lookalike, something like RevengeTouch. Objective-C is plain old C, and there's nothing more ducktypeful in the world than C; as long as noone checks the real class of the objects, which I believe noone does, you can make any class look like any other class. This opens up a possibility to synthesize any touches you want, with any coordinates you want.)

Backup strategy: there's TTScrollView, a sane reimplementation of UIScrollView in Three20 library. Unfortunately it feels very unnatural and non-iphonish to the user. But if every attempt of using UIScrollView fails, you can fall back to a custom-coded scroll view. I do recommend against it if at all possible; using UIScrollView ensures you are getting the native look-and-feel, no matter how it evolves in future iPhone OS versions.

Okay, this little essay got a little bit too long. I'm just still into UIScrollView games after the work on ScrollingMadness several days ago.

P.S. If you get any of these working and feel like sharing, please e-mail me the relevant code at andreyvit@gmail.com, I'd happily include it into my ScrollingMadness bag of tricks.

P.P.S. Adding this little essay to ScrollingMadness README.

这篇关于UIScrollView:水平分页,垂直滚动?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆