ios UIScrollView:水平分页,垂直滚动?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/728014/
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

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 15:47:44  来源:igfitidea点击:

UIScrollView: paging horizontally, scrolling vertically?

iosobjective-cuiscrollviewscroll-paging

提问by Andrey Tarantsov

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

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

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

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

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

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

回答by Andrey Tarantsov

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

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

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

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

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.

第一:嵌套的 UIScrollViews。坦率地说,我还没有看到一个人让这个工作。然而我个人还没有足够努力,我的实践表明,当你足够努力时,你可以让 UIScrollView 做任何你想做的事情

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 hitTestmethod and always returns itself, so that all touch events go into UIScrollView. Then inside touchesBegan, touchesMovedetc it checks if it's interested in the event, and either handles or passes it on to the inner components.

所以策略是让外部滚动视图只处理水平滚动,内部滚动视图只处理垂直滚动。要做到这一点,你必须知道 UIScrollView 在内部是如何工作的。它覆盖hitTest方法并始终返回自身,以便所有触摸事件进入 UIScrollView。然后里面touchesBegantouchesMoved等它检查它是否有兴趣的事件,并且或者手柄或将其传递到内部组件。

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

为了决定触摸是被处理还是转发,UIScrollView 在你第一次触摸它时启动一个计时器:

  • 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 notmoved your finger significantly within 150ms and UIScrollView started passing the events to the inner view, but thenyou have moved the finger far enough for the scrolling to begin, UIScrollView calls touchesCancelledon 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.

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

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

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

  • 如果你还没有150毫秒内显著移动你的手指的UIScrollView开始传递事件内视图,但随后您移动手指远远不够的滚动开始,UIScrollView的要求touchesCancelled在内部视图和开始滚动。

    请注意,当您触摸表格时,按住手指然后开始滚动,您触摸的行首先突出显示,然后取消突出显示。

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

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

  • If delaysContentTouchesis 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 cancelsTouchesis NO, then once the events are sent to a control, scrolling will never happen.
  • 如果delaysContentTouches为 NO,则不使用计时器 - 事件立即进入内部控件(但如果您将手指移得足够远,则会取消)
  • 如果cancelsTouches为 NO,则一旦将事件发送到控件,将永远不会发生滚动。

Note that it is UIScrollView that receives alltouchesBegin, touchesMoved, touchesEndedand touchesCanceledevents from CocoaTouch (because its hitTesttells it to do so). It then forwards them to the inner view if it wants to, as long as it wants to.

需要注意的是的UIScrollView接收所有touchesBegintouchesMovedtouchesEndedtouchesCanceled从CocoaTouch事件(因为它hitTest告诉它这样做)。然后,只要它愿意,它就会将它们转发到内部视图。

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.

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

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.

您希望将外部 UIScrollView 子类化(例如,您将类命名为 RemorsefulScrollView),以便它立即将所有事件转发到内部视图,而不是默认行为,并且仅当检测到显着的水平移动时,它才会滚动。

How to do make RemorsefulScrollView behave that way?

如何让 RemorsefulScrollView 表现得那样?

  • It looks like disabling vertical scrolling and setting delaysContentTouchesto 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 touchesBeganto UIScrollView, because you want it to remember a base coordinate for future horizontal scrolling (if you later decide it isa horizontal scrolling). You won't be able to send touchesBeganto UIScrollView later, because you cannot store the touchesargument: it contains objects that will be mutated before the next touchesMovedevent, and you cannot reproduce the old state.

    So you have to pass touchesBeganto UIScrollView immediately, but you will hide any further touchesMovedevents from it until you decide to scroll horizontally. No touchesMovedmeans no scrolling, so this initial touchesBeganwill do no harm. But do set delaysContentTouchesto NO, so that no additional surprise timers interfere.

    (Offtopic — unlike you, UIScrollView canstore touches properly and can reproduce and forward the original touchesBeganevent 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 touchesCancelledand touchesEnded. You have to turn touchesEndedinto touchesCancelled, however, because UIScrollView would interpret touchesBegan, touchesEndedsequence 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.

  • 看起来禁用垂直滚动并设置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,你也必须转发touchesCancelledtouchesEnded。你必须把touchesEndedtouchesCancelled,但是,因为UIScrollView中会解释touchesBegantouchesEnded序列为触摸点击,和将其转发到内部视图。您已经自己转发了正确的事件,因此您永远不希望 UIScrollView 转发任何内容。

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 touchesXxxreentry.

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

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 pagingEnabledproperty on the fly from your scrollViewDidScrolldelegate method.

第二种策略:如果由于某种原因嵌套的 UIScrollViews 不起作用或证明太难做对,您可以尝试只使用一个 UIScrollView,pagingEnabled从您的scrollViewDidScroll委托方法动态切换其属性。

To prevent diagonal scrolling, you should first try remembering contentOffset in scrollViewWillBeginDragging, and checking and resetting contentOffset inside scrollViewDidScrollif 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.)

为了防止对角滚动,您应该首先尝试记住 contentOffset in scrollViewWillBeginDraggingscrollViewDidScroll如果检测到对角移动,则检查并重置里面的 contentOffset 。另一种尝试的策略是将 contentSize 重置为仅在一个方向上启用滚动,一旦您决定了用户手指的方向。( UIScrollView 似乎非常宽容地从其委托方法中摆弄 contentSize 和 contentOffset 。)

If that does not work either or results in sloppy visuals, you have to override touchesBegan, touchesMovedetc 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.)

如果不以马虎的视觉效果更好地工作,或结果,你必须重写touchesBegantouchesMoved等等,而不是向对角线移动事件的UIScrollView。(然而,在这种情况下,用户体验将是次优的,因为您将不得不忽略对角线移动,而不是将它们强制为单一方向。如果您真的喜欢冒险,您可以编写自己的 UITouch 外观,类似于 RevengeTouch。目标-C 是普通的老 C,世界上没有什么比 C 更像鸭子的了;只要没有人检查对象的真实类,我相信没有人这样做,你可以让任何类看起来像任何其他类。这就打开了可以用您想要的任何坐标合成您想要的任何触摸。)

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.

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

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.

好吧,这篇小文章有点太长了。几天前在 ScrollingMadness 上工作后,我仍然喜欢 UIScrollView 游戏。

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

PS 如果您使用了其中的任何一个并愿意分享,请将相关代码通过电子邮件发送至 [email protected],我很乐意将其包含在我的ScrollingMadness 技巧包中

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

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

回答by Tonetel

This works for me everytime...

这对我每次都有效...

scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * NumberOfPages, 1);

回答by icompot

For the lazy people like me:

对于像我这样的懒人:

Set scrollview.directionalLockEnabledto YES

设置scrollview.directionalLockEnabledYES

- (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;
}

回答by Yildiray Meric

I used following method to solve this problem, hope it helps you.

我用下面的方法解决了这个问题,希望对你有帮助。

First define following variables in your controllers header file.

首先在您的控制器头文件中定义以下变量。

CGPoint startPos;
int     scrollDirection;

startPoswill keep the contentOffsetvalue when your delegate receives scrollViewWillBeginDraggingmessage. So in this method we do this;

当您的委托收到scrollViewWillBeginDragging消息时,startPos将保留contentOffset值。所以在这个方法中我们这样做;

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    startPos = scrollView.contentOffset;
    scrollDirection=0;
}

then we use these values to determine users intended scroll direction in scrollViewDidScrollmessage.

然后我们使用这些值来确定用户在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)<abs(startPos.y-scrollView.contentOffset.y)){          
          NSLog(@"Vertical Scrolling");
          scrollDirection=1;
       } else {
          NSLog(@"Horitonzal Scrolling");
          scrollDirection=2;
       }
    }
//Update scroll position of the scrollview according to detected direction.     
    if (scrollDirection==1) {
       [scrollView setContentOffset:CGPointMake(startPos.x,scrollView.contentOffset.y) animated:NO];
    } else if (scrollDirection==2){
       [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x,startPos.y) animated:NO];
    }
 }

finally we have to stop all update operations when user end dragging;

最后,当用户结束拖动时,我们必须停止所有更新操作;

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (decelerate) {
       scrollDirection=3;
    }
 }

回答by James J

I might be gravedigging here, but I stumbled onto this post today as I was trying to solve the same problem. Here's my solution, seems to be working great.

我可能在这里挖坟,但我今天偶然发现了这篇文章,因为我试图解决同样的问题。这是我的解决方案,似乎效果很好。

I want to display a screenful of content, but allow the user to scroll to one of 4 other screenfuls, up down left and right. I have a single UIScrollView with size 320x480 and a contentSize of 960x1440. The content offset starts at 320,480. In scrollViewDidEndDecelerating:, I redraw the 5 views (center views and the 4 around it), and reset the content offset to 320,480.

我想显示一屏内容,但允许用户滚动到其他 4 个屏幕之一,左右上下。我有一个大小为 320x480 和 contentSize 为 960x1440 的 UIScrollView。内容偏移量从 320,480 开始。在 scrollViewDidEndDecelerating: 中,我重绘了 5 个视图(中心视图及其周围的 4 个视图),并将内容偏移量重置为 320,480。

Here's the meat of my solution, the scrollViewDidScroll: method.

这是我的解决方案的主要内容, 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);
    }
}

回答by julianlubenov

Guys it is as simple as making the subview height the same as the scroll view height and the content size height. Then the vertical scrolling is disabled.

伙计们,就像使子视图高度与滚动视图高度和内容大小高度相同一样简单。然后垂直滚动被禁用。

回答by Mattias Wadman

Here is my implementation of a paged UIScrollViewthat only allow orthogonal scrolls. Be sure to set pagingEnabledto YES.

这是我对UIScrollView只允许正交滚动的分页的实现。请务必设置pagingEnabledYES

PagedOrthoScrollView.h

PagedOrthoScrollView.h

@interface PagedOrthoScrollView : UIScrollView
@end

PagedOrthoScrollView.m

PagedOrthoScrollView.m

#import "PagedOrthoScrollView.h"

typedef enum {
  PagedOrthoScrollViewLockDirectionNone,
  PagedOrthoScrollViewLockDirectionVertical,
  PagedOrthoScrollViewLockDirectionHorizontal
} PagedOrthoScrollViewLockDirection; 

@interface PagedOrthoScrollView ()
@property(nonatomic, assign) PagedOrthoScrollViewLockDirection dirLock;
@property(nonatomic, assign) CGFloat valueLock;
@end

@implementation PagedOrthoScrollView
@synthesize dirLock;
@synthesize valueLock;

- (id)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self == nil) {
    return self;
  }

  self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  self.valueLock = 0;

  return self;
}

- (void)setBounds:(CGRect)bounds {
  int mx, my;

  if (self.dirLock == PagedOrthoScrollViewLockDirectionNone) {
    // is on even page coordinates, set dir lock and lock value to closest page 
    mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
    if (mx != 0) {
      self.dirLock = PagedOrthoScrollViewLockDirectionHorizontal;
      self.valueLock = (round(CGRectGetMinY(bounds) / CGRectGetHeight(self.bounds)) *
                        CGRectGetHeight(self.bounds));
    } else {
      self.dirLock = PagedOrthoScrollViewLockDirectionVertical;
      self.valueLock = (round(CGRectGetMinX(bounds) / CGRectGetWidth(self.bounds)) *
                        CGRectGetWidth(self.bounds));
    }

    // show only possible scroll indicator
    self.showsVerticalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionVertical;
    self.showsHorizontalScrollIndicator = dirLock == PagedOrthoScrollViewLockDirectionHorizontal;
  }

  if (self.dirLock == PagedOrthoScrollViewLockDirectionHorizontal) {
    bounds.origin.y = self.valueLock;
  } else {
    bounds.origin.x = self.valueLock;
  }

  mx = abs((int)CGRectGetMinX(bounds) % (int)CGRectGetWidth(self.bounds));
  my = abs((int)CGRectGetMinY(bounds) % (int)CGRectGetHeight(self.bounds));

  if (mx == 0 && my == 0) {
    // is on even page coordinates, reset lock
    self.dirLock = PagedOrthoScrollViewLockDirectionNone;
  }

  [super setBounds:bounds];
}

@end

回答by Kurt Horimoto

I've also had to solve this problem, and while Andrey Tarantsov's answer definitely has a lot of useful information for understanding how UIScrollViewswork, I feel that the solution is a bit over-complicated. My solution, which doesn't involve any subclassing or touch forwarding, is as follows:

我也不得不解决这个问题,虽然安德烈塔兰索夫的回答肯定有很多有用的信息来理解如何UIScrollViews工作,但我觉得这个解决方案有点过于复杂。我的解决方案不涉及任何子类化或触摸转发,如下所示:

  1. Create two nested scroll views, one for each direction.
  2. Create two dummy UIPanGestureRecognizers, again one for each direction.
  3. Create failure dependencies between the UIScrollViews' panGestureRecognizer properties and the dummy UIPanGestureRecognizercorresponding to the direction perpendicular to your UIScrollView's desired scrolling direction by using UIGestureRecognizer's-requireGestureRecognizerToFail:selector.
  4. In the -gestureRecognizerShouldBegin:callbacks for your dummy UIPanGestureRecognizers, calculate the initial direction of the pan using the gesture's -translationInView: selector (your UIPanGestureRecognizerswon't call this delegate callback until your touch has translated enough to register as a pan). Allow or disallow your gesture recognizer to begin depending on the calculated direction, which in turn should control whether or not the perpendicular UIScrollView pan gesture recognizer will be allowed to begin.
  5. In -gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:, don't allow your dummy UIPanGestureRecognizersto run alongside scrolling (unless you have some extra behavior you wanted to add).
  1. 创建两个嵌套滚动视图,每个方向一个。
  2. 创建两个 dummy UIPanGestureRecognizers,每个方向一个。
  3. 使用选择器在UIScrollViews' panGestureRecognizer 属性和与UIPanGestureRecognizer垂直于 UIScrollView 所需滚动方向的方向对应的虚拟对象之间创建失败依赖关系UIGestureRecognizer's-requireGestureRecognizerToFail:
  4. -gestureRecognizerShouldBegin:您的虚拟 UIPanGestureRecognizers的回调中,使用手势的 -translationInView: 选择器计算平移的初始方向(在UIPanGestureRecognizers您的触摸转换到足以注册为平移之前,您不会调用此委托回调)。根据计算的方向允许或禁止您的手势识别器开始,这反过来应该控制是否允许垂直 UIScrollView 平移手势识别器开始。
  5. 在 中-gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:,不要让你的虚拟对象UIPanGestureRecognizers与滚动一起运行(除非你想添加一些额外的行为)。

回答by Jeremy

Thanks Tonetel. I slightly modified your approach, but it was exactly what I needed to prevent horizontal scrolling.

谢谢托内特尔。我稍微修改了您的方法,但这正是我防止水平滚动所需要的。

self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 1);

回答by Jon Conner

What I did was create a paging UIScrollView with a frame the size of the view screen and set the content size to the width needed for all of my content and a height of 1 (thanks Tonetel). Then I created menu UIScrollView pages with frames set to each page, the size of the view screen, and set the content size of each to the width of the screen and the necessary height for each one. Then I simply added each of the menu pages to the paging view. Make sure paging is enabled on the paging scroll view but disabled on the menu views (should be disabled by default) and you should be good to go.

我所做的是创建一个分页 UIScrollView,其框架与视图屏幕的大小相同,并将内容大小设置为我所有内容所需的宽度和 1(感谢 Tonetel)。然后我创建了菜单 UIScrollView 页面,框架设置为每个页面,视图屏幕的大小,并将每个页面的内容大小设置为屏幕的宽度和每个页面的必要高度。然后我简单地将每个菜单页面添加到分页视图中。确保在分页滚动视图上启用分页,但在菜单视图上禁用(默认情况下应禁用),您应该很高兴。

The paging scroll view now scrolls horizontally, but not vertically because of the content height. Each page scrolls vertically but not horizontally because of its content size constraints as well.

分页滚动视图现在水平滚动,但由于内容高度而不是垂直滚动。由于其内容大小的限制,每个页面都垂直滚动而不是水平滚动。

Here's the code if that explanation left something to be desired:

如果该解释有待改进,这是代码:

UIScrollView *newPagingScrollView =  [[UIScrollView alloc] initWithFrame:self.view.bounds]; 
[newPagingScrollView setPagingEnabled:YES];
[newPagingScrollView setShowsVerticalScrollIndicator:NO];
[newPagingScrollView setShowsHorizontalScrollIndicator:NO];
[newPagingScrollView setDelegate:self];
[newPagingScrollView setContentSize:CGSizeMake(self.view.bounds.size.width * NumberOfDetailPages, 1)];
[self.view addSubview:newPagingScrollView];

float pageX = 0;

for (int i = 0; i < NumberOfDetailPages; i++)
{               
    CGRect pageFrame = (CGRect) 
    {
        .origin = CGPointMake(pageX, pagingScrollView.bounds.origin.y), 
        .size = pagingScrollView.bounds.size
    };
    UIScrollView *newPage = [self createNewPageFromIndex:i ToPageFrame:pageFrame]; // newPage.contentSize custom set in here        
    [pagingScrollView addSubview:newPage];

    pageX += pageFrame.size.width;  
}