UIScrollView水平分页,例如Mobile Safari标签


88

移动Safari允许您通过输入一种UIScrollView水平分页视图(底部带有页面控件)来切换页面。

我试图复制这种特殊的行为,其中水平可滚动的UIScrollView显示下一个视图的某些内容。

苹果提供的示例:PageControl显示了如何使用UIScrollView进行水平分页,但是所有视图都占据整个屏幕宽度。

如何获得UIScrollView来显示下一个视图的某些内容(如移动Safari一样)?


2
只是想一想...尝试使滚动视图的边界小于屏幕的边界,并努力使视图正确显示。(并将滚动视图的clipsToBounds设置为“否”)
mjhoy,2009年

我想让页面的尺寸大于uiscrollview的宽度(水平滚动)。mjhoy的想法实际上帮助了我!
Sasho

Answers:


263

UIScrollView启用了分页的A 将停止在其帧宽度(或高度)的倍数处。因此,第一步是弄清楚您希望页面的宽度。使宽度为UIScrollView。然后,根据需要设置子视图的大小,然后根据UIScrollView宽度的倍数设置其中心。

然后,因为要查看其他页,当然,设置clipsToBoundsNO作为mhjoy说明。现在,技巧部分是让用户在UIScrollView框的范围之外开始拖动时可以滚动。我的解决方案(当我最近必须这样做时)如下:

创建将包含和的子视图的子UIView类(即ClipViewUIScrollView。本质上,它应该具有您UIScrollView在正常情况下会假设的框架。将放置UIScrollView在的中心ClipView。确保ClipViewclipsToBounds设定为YES如果其宽度小于其父视图。另外,ClipView需要参考UIScrollView

最后一步是重写- (UIView *)hitTest:withEvent:里面ClipView

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  return [self pointInside:point withEvent:event] ? scrollView : nil;
}

基本上UIScrollView,这正是您所需要的将触摸区域扩展到其父视图的框架。

另一个选择是子类化UIScrollView并重写其- (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) event方法,但是您仍将需要容器视图来进行裁剪,并且可能YES仅基于UIScrollView的帧来确定何时返回的难度很大。

注意: 如果子视图用户交互存在问题,您还应该查看Juri Pakaste的hitTest:withEvent:修改


3
谢谢爱德。我使用了一个UIScrollView子类来延迟加载视图(在中layoutSubviews),并使用clippingRect进行了pointInside:withEvent:测试。效果很好,不需要其他容器视图。
ohhorob

1
好答案。我使用了UIScrollView@ohhorob之类的子类,但具有用于裁剪和返回[super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];in 的容器视图pointInside:withEvent:。这非常有效,并且@JuriPakaste的答案中没有提到用户交互问题。
cduck 2011年

1
哇,这是一个非常好的解决方案。不过,在我的设置中,仅一件事是我要添加的页面具有UIButtons。当我覆盖hitTest方法时,不允许我点击这些按钮。我可以滚动,但是在任何页面内都不能选择任何动作。
Salcedo

8
对于最近遇到这种情况的人,请注意,- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset在iOS5中将其添加到UIScrollViewDelegate意味着您不必再经历这个麻烦了。
Mike Pollard

1
@MikePollard的效果并不相同,targetContentOffset不太适合这种情况。
凯尔·芳

70

ClipView上面的解决方案对我有用,但是我必须执行不同的-[UIView hitTest:withEvent:]实现。埃德·马蒂(Ed Marty)的版本无法在水平滚动条中进行垂直滚动视图的情况下进行用户交互。

以下版本对我有用:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}

这也有助于在用户交互正常工作时获取按钮和其他子视图。:)谢谢
Thomas Clayson

4
感谢您提供此代码,如何使用此修改从滚动视图边界中未包含的子视图(按钮)中获取事件?例如,如果按钮在中央滚动视图的边界内但不在外部,则可以使用。
DreamOfMirrors,2011年

4
这确实有帮助。它应作为对所选答案的修改而包括在内。
Pacu

2
是! 这也使滚动视图内的用户交互再次起作用!为了做到这一点,我确实必须在ClipView上将userInteractionEnabled设置为YES。
Tom van Zummeren 2011年

我必须遍历self.scrollView.subviews并首先执行hitTest。
鲍磊

8

将scrollView的框架大小设置为页面大小:

[self.view addSubview:scrollView];
[self.view addGestureRecognizer:mainScrollView.panGestureRecognizer];

现在,您可以平移self.view,并且scrollView 上的内容将被滚动。
也可scrollView.clipsToBounds = NO;用于防止剪切内容。


5

我自己决定使用自定义UIScrollView,因为它对我来说是最快,最简单的方法。但是,我没有看到任何确切的代码,因此我想分享一下。我需要的是内容较小的UIScrollView,因此UIScrollView本身很小,无法实现页面调度效果。如该帖子所述,您不能滑动。但是现在可以了。

创建一个类CustomScrollView和子类UIScrollView。然后,您需要做的就是将其添加到.m文件中:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  return (point.y >= 0 && point.y <= self.frame.size.height);
}

这使您可以从一侧滚动到另一侧(水平)。相应地调整范围以设置滑动/滚动触摸区域。请享用!


4

我做了另一个实现,可以自动返回scrollview。因此,它不需要IBOutlet,它会限制项目中的重用。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self pointInside:point withEvent:event]) {
        for (id view in [self subviews]) {
            if ([view isKindOfClass:[UIScrollView class]]) {
                return view;
            }
        }
    }
    return nil;
}

1
如果您已经有xib / storyboard,则可以使用此选项。否则,我不确定您将如何获得对Paging / Scrollview的引用。有任何想法吗?
mskw 2012年

3

对于ClipView hitTest实现,我还有另一个可能有用的修改。我不想提供对ClipView的UIScrollView引用。我在下面的实现允许您重新使用ClipView类来扩展任何内容的点击测试范围,而不必为其提供引用。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (([self pointInside:point withEvent:event]) && (self.subviews.count >= 1))
    {
        // An extended-hit view should only have one sub-view, or make sure the
        // first subview is the one you want to expand the hit test for.
        return [self.subviews objectAtIndex:0];
    }

    return nil;
}

3

我实现了上面建议的建议,但是UICollectionView我使用的是超出屏幕范围的任何东西。这导致附近的单元仅在用户向其滚动时才超出范围,这并不理想。

我最终要做的是通过将以下方法添加到委托(或UICollectionViewLayout)中来模拟滚动视图的行为。

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{    
  if (velocity.x > 0) {
    proposedContentOffset.x = ceilf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }
  else {
    proposedContentOffset.x = floorf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }

  return proposedContentOffset;
}

这完全避免了滑动动作的下放,这也是一个奖励。的UIScrollViewDelegate有一个名为类似的方法,scrollViewWillEndDragging:withVelocity:targetContentOffset:它可以用于网页UITableViews和UIScrollViews。


2
戴夫,我也在努力使用UICollectionView实现这种行为,最终我使用了您在此处描述的相同方法。但是,滚动体验不如启用分页功能的滚动视图好。当我以更大的速度滑动时,滚动了几页,并在建议的页面上停止了。我想要实现的是将行为保存为启用分页的普通滚动视图-无论我使用哪种速度,我都只会得到1页。你有什么想法吗?
Peter Stajger 2013年

3

在支持此SO问题的技术的同时,在滚动视图的子视图上启用触发点击事件。使用对滚动视图的引用(self.scrollView)以提高可读性。

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = nil;
    NSArray *childViews = [self.scrollView subviews];
    for (NSUInteger i = 0; i < childViews.count; i++) {
        CGRect childFrame = [[childViews objectAtIndex:i] frame];
        CGRect scrollFrame = self.scrollView.frame;
        CGPoint contentOffset = self.scrollView.contentOffset;
        if (childFrame.origin.x + scrollFrame.origin.x < point.x + contentOffset.x &&
            point.x + contentOffset.x < childFrame.origin.x + scrollFrame.origin.x + childFrame.size.width &&
            childFrame.origin.y + scrollFrame.origin.y < point.y + contentOffset.y &&
            point.y + contentOffset.y < childFrame.origin.y + scrollFrame.origin.y + childFrame.size.height
        ){
            hitView = [childViews objectAtIndex:i];
            return hitView;
        }
    }
    hitView = [super hitTest:point withEvent:event];
    if (hitView == self)
        return self.scrollView;
    return hitView;
}

将此添加到您的子视图以捕获触摸事件:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

(这是user1856273解决方案的一个变体。已进行了清理以提高可读性,并结合了Bartserk的错误修复。我曾考虑过编辑user1856273的答案,但此更改太大了。)


为了使用子视图中嵌入的UIButton进行工作,我替换了代码hitView = [childViews objectAtIndex:i]; // //在[[childViews objectAtIndex:i]子视图]的UIView * paneView中找到UIButton){if([[paneView isKindOfClass:[UIButton class]])return paneView; }
CharlesA

2

我的版本按下滚动条上的按钮-work =)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView* child = nil;
    for (int i=0; i<[[[[self subviews] objectAtIndex:0] subviews] count];i++) {

        CGRect myframe =[[[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i] frame];
        CGRect myframeS =[[[self subviews] objectAtIndex:0] frame];
        CGPoint myscroll =[[[self subviews] objectAtIndex:0] contentOffset];
        if (myframe.origin.x < point.x && point.x < myframe.origin.x+myframe.size.width &&
            myframe.origin.y+myframeS.origin.y < point.y+myscroll.y && point.y+myscroll.y < myframe.origin.y+myframeS.origin.y +myframe.size.height){
            child = [[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i];
            return child;
        }


    }

    child = [super hitTest:point withEvent:event];
    if (child == self)
        return [[self subviews] objectAtIndex:0];
    return child;
    }

但只有[[self subviews] objectAtIndex:0]必须是滚动


这解决了我的问题:)请注意,在检查边界时,没有将滚动偏移量添加到point.x,这使代码在水平滚动上无法正常工作。添加完之后,就可以了!
Bartserk

2

这是一个基于Ed Marty的答案的Swift答案,还包括Juri Pakaste所做的修改,允许在滚动视图内点击按钮等。

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}

感谢您更新:)
Liam Bolling
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.