以比帧尺寸更小的增量分页UIScrollView

我有一个滚动视图,是屏幕的宽度,但只有约70像素高。 它包含许多50 x 50的图标(与他们周围的空间),我希望用户能够从中select。 但是,我总是希望滚动视图以分页的方式运行,并始终以精确中心的图标停止。

如果图标是屏幕的宽度,这不会是一个问题,因为UIScrollView的分页将照顾它。 但是因为我的小图标比内容大小要小得多,所以不起作用。

我之前在应用程序调用AllRecipes中看到过这种行为。 我只是不知道该怎么做。

任何关于如何在每个图标大小的基础上进行分页的想法?

尝试使您的滚动视图小于屏幕的大小(宽度),但取消IB中的“剪辑子视图”checkbox。 然后,覆盖一个透明的,userInteractionEnabled = NO视图(全宽),覆盖hitTest:withEvent:返回你的滚动视图。 这应该给你你想要的。 看到这个答案的更多细节。

还有另一种解决scheme,可能比用另一个视图覆盖滚动视图和覆盖hitTest更好一点。

您可以inheritanceUIScrollView并覆盖它的pointInside。 然后滚动视图可以响应其框架外的触摸。 当然其余的都是一样的。

@interface PagingScrollView : UIScrollView { UIEdgeInsets responseInsets; } @property (nonatomic, assign) UIEdgeInsets responseInsets; @end @implementation PagingScrollView @synthesize responseInsets; - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { CGPoint parentLocation = [self convertPoint:point toView:[self superview]]; CGRect responseRect = self.frame; responseRect.origin.x -= responseInsets.left; responseRect.origin.y -= responseInsets.top; responseRect.size.width += (responseInsets.left + responseInsets.right); responseRect.size.height += (responseInsets.top + responseInsets.bottom); return CGRectContainsPoint(responseRect, parentLocation); } @end 

被接受的答案是非常好的,但它只适用于UIScrollView类,而不是它的后代。 例如,如果你有很多的视图,并转换为UICollectionView ,你将无法使用此方法,因为集合视图将删除它认为是“不可见”的视图(即使它们没有被剪切,会消失)。

有关提到scrollViewWillEndDragging:withVelocity:targetContentOffset:在我看来是正确的答案。

你可以做的是,在这个委托方法中,你计算当前页面/索引。 然后您决定速度和目标偏移是否值得“下一页”移动。 你可以非常接近pagingEnabled行为。

注意:我通常是RubyMotion开发者,所以有人请certificate这个Obj-C代码的正确性。 对不起,混合使用camelCase和snake_case,我复制并粘贴了大部分代码。

 - (void) scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetOffset { CGFloat x = targetOffset->x; int index = [self convertXToIndex: x]; CGFloat w = 300f; // this is your custom page width CGFloat current_x = w * [self convertXToIndex: scrollView.contentOffset.x]; // even if the velocity is low, if the offset is more than 50% past the halfway // point, proceed to the next item. if ( velocity.x < -0.5 || (current_x - x) > w / 2 ) { index -= 1 } else if ( velocity.x > 0.5 || (x - current_x) > w / 2 ) { index += 1; } if ( index >= 0 || index < self.items.length ) { CGFloat new_x = [self convertIndexToX: index]; targetOffset->x = new_x; } } 

我看到很多解决scheme,但是它们非常复杂。 有一个小页面,但仍然保持所有区域可滚动的简单方法是使滚动更小,并将scrollView.panGestureRecognizer移动到您的父视图。 这些步骤是:

  1. 减less你的scrollView大小 ScrollView的大小比父级小

  2. 确保你的滚动视图分页,不剪辑子视图 在这里输入图像说明

  3. 在代码中,将scrollview平移手势移动到全宽的父容器视图:

  override func viewDidLoad() { super.viewDidLoad() statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer) } 

查看UIScrollViewDelegate上的-scrollView:didEndDragging:willDecelerate:方法。 就像是:

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { int x = scrollView.contentOffset.x; int xOff = x % 50; if(xOff < 25) x -= xOff; else x += 50 - xOff; int halfW = scrollView.contentSize.width / 2; // the width of the whole content view, not just the scroll view if(x > halfW) x = halfW; [scrollView setContentOffset:CGPointMake(x,scrollView.contentOffset.y)]; } 

这是不完美的 – 最后我尝试了这个代码,当我从一个橡皮筋卷轴返回时,我得到了一些丑陋的行为(跳跃,我记得)。 您可以通过简单地将滚动视图的bounces属性设置为NO来避免这种情况。

因为我似乎没有被允许发表评论,所以我会在这里添加我对诺亚的回答的评论。

我已经用诺亚·威瑟斯庞(Noah Witherspoon)描述的方法成功地实现了这一点。 当滚动视图超过其边缘时,我只是简单地不调用setContentOffset:方法来解决跳跃问题。

  - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { // Don't snap when at the edges because that will override the bounce mechanic if (self.contentOffset.x < 0 || self.contentOffset.x + self.bounds.size.width > self.contentSize.width) return; ... } 

我还发现我需要在UIScrollViewDelegate实现-scrollViewWillBeginDecelerating:方法来捕获所有的情况。

我尝试了上面用pointInside覆盖透明视图的解决scheme:withEvent:overridden。 这对我来说工作得非常好,但是在某些情况下可能会崩溃 – 请参阅我的评论。 我结束了自己实现分页与scrollViewDidScroll组合来跟踪当前页面索引和scrollViewWillEndDragging:withVelocity:targetContentOffset和scrollViewDidEndDragging:willDecelerate捕捉到适当的页面。 请注意,will-end方法仅适用于iOS5 +,但是如果速度!= 0,则针对特定的偏移量是非常好的。特别是,如果有速度,则可以通过animation告诉调用者滚动视图的位置特定的方向。

 - (void) scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetOffset { static CGFloat previousIndex; CGFloat x = targetOffset->x + kPageOffset; int index = (x + kPageWidth/2)/kPageWidth; if(index<previousIndex - 1){ index = previousIndex - 1; }else if(index > previousIndex + 1){ index = previousIndex + 1; } CGFloat newTarget = index * kPageWidth; targetOffset->x = newTarget - kPageOffset; previousIndex = index; } 

kPageWidth是你想要的页面的宽度。 kPageOffset是如果你不希望细胞左alignment(即如果你希望他们是中心alignment,设置为你的细胞的一半宽度)。 否则,它应该是零。

这也将只允许一次滚动一页。

创build滚动视图时,请确保设置为:

 scrollView.showsHorizontalScrollIndicator = false; scrollView.showsVerticalScrollIndicator = false; scrollView.pagingEnabled = true; 

然后将您的子视图添加到滚动条的偏移量等于滚动条的索引*高度。 这是一个垂直滚动:

 UIView * sub = [UIView new]; sub.frame = CGRectMake(0, index * h, w, subViewHeight); [scrollView addSubview:sub]; 

如果你现在运行它的视图是分开的,并启用分页,他们一次滚动一个。

所以然后把它放在你的viewDidScroll方法中:

  //set vars int index = scrollView.contentOffset.y / h; //current index float y = scrollView.contentOffset.y; //actual offset float p = (y / h)-index; //percentage of page scroll complete (0.0-1.0) int subViewHeight = h-240; //height of the view int spacing = 30; //preferred spacing between views (if any) NSArray * array = scrollView.subviews; //cycle through array for (UIView * sub in array){ //subview index in array int subIndex = (int)[array indexOfObject:sub]; //moves the subs up to the top (on top of each other) float transform = (-h * subIndex); //moves them back down with spacing transform += (subViewHeight + spacing) * subIndex; //adjusts the offset during scroll transform += (h - subViewHeight - spacing) * p; //adjusts the offset for the index transform += index * (h - subViewHeight - spacing); //apply transform sub.transform = CGAffineTransformMakeTranslation(0, transform); } 

子视图的框架仍然分开,我们只是在用户滚动时通过变换将它们一起移动。

此外,您可以访问上面的variablesp,您可以使用其他的东西,如子视图中的alpha或变换。 当p == 1时,该页面被完全显示出来,或者更倾向于1。

尝试使用scrollView的contentInset属性:

 scrollView.pagingEnabled = YES; [scrollView setContentSize:CGSizeMake(height, pageWidth * 3)]; double leftContentOffset = pageWidth - kSomeOffset; scrollView.contentInset = UIEdgeInsetsMake(0, leftContentOffset, 0, 0); 

这可能需要一些玩弄你的价值观来实现所需的分页。

与发布的替代scheme相比,我发现这个工作更干净。 使用scrollViewWillEndDragging:问题scrollViewWillEndDragging:委托方法是加速的慢动作是不自然的。

这是解决问题的唯一办法。

 import UIKit class TestScrollViewController: UIViewController, UIScrollViewDelegate { var scrollView: UIScrollView! var cellSize:CGFloat! var inset:CGFloat! var preX:CGFloat=0 let pages = 8 override func viewDidLoad() { super.viewDidLoad() cellSize = (self.view.bounds.width-180) inset=(self.view.bounds.width-cellSize)/2 scrollView=UIScrollView(frame: self.view.bounds) self.view.addSubview(scrollView) for i in 0..<pages { let v = UIView(frame: self.view.bounds) v.backgroundColor=UIColor(red: CGFloat(CGFloat(i)/CGFloat(pages)), green: CGFloat(1 - CGFloat(i)/CGFloat(pages)), blue: CGFloat(CGFloat(i)/CGFloat(pages)), alpha: 1) v.frame.origin.x=CGFloat(i)*cellSize v.frame.size.width=cellSize scrollView.addSubview(v) } scrollView.contentSize.width=cellSize*CGFloat(pages) scrollView.isPagingEnabled=false scrollView.delegate=self scrollView.contentInset.left=inset scrollView.contentOffset.x = -inset scrollView.contentInset.right=inset } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { preX = scrollView.contentOffset.x } func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let originalIndex = Int((preX+cellSize/2)/cellSize) let targetX = targetContentOffset.pointee.x var targetIndex = Int((targetX+cellSize/2)/cellSize) if targetIndex > originalIndex + 1 { targetIndex=originalIndex+1 } if targetIndex < originalIndex - 1 { targetIndex=originalIndex - 1 } if velocity.x == 0 { let currentIndex = Int((scrollView.contentOffset.x+self.view.bounds.width/2)/cellSize) let tx=CGFloat(currentIndex)*cellSize-(self.view.bounds.width-cellSize)/2 scrollView.setContentOffset(CGPoint(x:tx,y:0), animated: true) return } let tx=CGFloat(targetIndex)*cellSize-(self.view.bounds.width-cellSize)/2 targetContentOffset.pointee.x=scrollView.contentOffset.x UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: [UIViewAnimationOptions.curveEaseOut, UIViewAnimationOptions.allowUserInteraction], animations: { scrollView.contentOffset=CGPoint(x:tx,y:0) }) { (b:Bool) in } } } 

这是我的答案。 在我的例子中,有一个section header的collectionView是我们想要使它具有自定义的isPagingEnabled效果的isPagingEnabled ,而cell的高度是一个常数值。

 var isScrollingDown = false // in my example, scrollDirection is vertical var lastScrollOffset = CGPoint.zero func scrollViewDidScroll(_ sv: UIScrollView) { isScrollingDown = sv.contentOffset.y > lastScrollOffset.y lastScrollOffset = sv.contentOffset } // 实现 isPagingEnabled 效果func scrollViewWillEndDragging(_ sv: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { let realHeaderHeight = headerHeight + collectionViewLayout.sectionInset.top guard realHeaderHeight < targetContentOffset.pointee.y else { // make sure that user can scroll to make header visible. return // 否则无法手动滚到顶部} let realFooterHeight: CGFloat = 0 let realCellHeight = cellHeight + collectionViewLayout.minimumLineSpacing guard targetContentOffset.pointee.y < sv.contentSize.height - realFooterHeight else { // make sure that user can scroll to make footer visible return // 若有footer,不在此处 return 会导致无法手动滚动到底部} let indexOfCell = (targetContentOffset.pointee.y - realHeaderHeight) / realCellHeight // velocity.y can be 0 when lifting your hand slowly let roundedIndex = isScrollingDown ? ceil(indexOfCell) : floor(indexOfCell) // 如果松手时滚动速度为 0,则 velocity.y == 0,且 sv.contentOffset == targetContentOffset.pointee let y = realHeaderHeight + realCellHeight * roundedIndex - collectionViewLayout.minimumLineSpacing targetContentOffset.pointee.y = y }