如何使UIScrollView对象图标(如App Store:Feature)

我想要得到的是此滚动视图具有的相同行为:

App Store功能滚动视图(左)

我知道这是使用HTML而不是本机API,但我正在尝试将其实现为UIKit组件。

现在,我正在寻找的行为:

  • 请注意,它是一个分页滚动视图,但“页面大小”小于视图的宽度。
  • 当您从左向右滚动时,每个页面“捕捉”到最左侧的项目。
  • 当您从右端向左滚动时,它会“捕捉”到最右侧的项目。

同一页面,但现在从右到左:

App Store功能滚动视图(右)

我尝试过的:

  • 我已经尝试使滚动视图小于它的超级视图并覆盖hitTest,这让我从左到右的行为。
  • 我已经尝试实现scrollViewWillEndDragging:withVelocity:targetContentOffset:并设置我想要的targetContentOffset,但由于我无法改变速度,它只是滚动太慢或太快。
  • 我已经尝试实现scrollViewDidEndDecelerating:然后动画到正确的偏移量但滚动视图先停止然后移动,它看起来不自然。
  • 我已经尝试实现scrollViewDidEndDragging:willDecelerate:然后设置动画到正确的偏移但滚动视图“跳转”并且没有正确动画。

我没有想法。

谢谢!

更新:

我最终使用Rob Mayoff的方法,看起来很干净。 我更改了它,因此当速度为0时它会起作用,例如当用户拖动,停止和释放手指时。

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(CGPoint *)targetContentOffset { CGFloat maxOffset = scrollView.contentSize.width - scrollView.bounds.size.width; CGFloat minOffset = 0; if (velocity.x == 0) { CGFloat targetX = MAX(minOffset,MIN(maxOffset, targetContentOffset->x)); CGFloat diff = targetX - baseOffset; if (ABS(diff) > offsetStep/2) { if (diff > 0) { //going left baseOffset = MIN(maxOffset, baseOffset + offsetStep); } else { //going right baseOffset = MAX(minOffset, baseOffset - offsetStep); } } } else { if (velocity.x > 0) { baseOffset = MIN(maxOffset, baseOffset + offsetStep); } else { baseOffset = MAX(minOffset, baseOffset - offsetStep); } } targetContentOffset->x = baseOffset; } 

此解决方案的唯一问题是滑动滚动视图不会产生“反弹”效果。 感觉“卡住了”。

设置scrollView.decelerationRate = UIScrollViewDecelerationRateFast ,结合实现scrollViewWillEndDragging:withVelocity:targetContentOffset:似乎对我使用集合视图。

首先,我给自己一些实例变量:

 @implementation ViewController { NSString *cellClassName; CGFloat baseOffset; CGFloat offsetStep; } 

viewDidLoad ,我设置了视图的decelerationRate

 - (void)viewDidLoad { [super viewDidLoad]; cellClassName = NSStringFromClass([MyCell class]); [self.collectionView registerNib:[UINib nibWithNibName:cellClassName bundle:nil] forCellWithReuseIdentifier:cellClassName]; self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast; } 

我需要offsetStep为适合视图屏幕边界的整数项目的大小。 我在viewDidLayoutSubviews计算它:

 - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout; CGFloat stepUnit = layout.itemSize.width + layout.minimumLineSpacing; offsetStep = stepUnit * floorf(self.collectionView.bounds.size.width / stepUnit); } 

在滚动开始之前,我需要将baseOffset作为视图的X偏移量。 我在viewDidAppear:初始化它:

 - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; baseOffset = self.collectionView.contentOffset.x; } 

然后我需要强制视图以offsetStep步骤滚动。 我在scrollViewWillEndDragging:withVelocity:targetContentOffset:执行此scrollViewWillEndDragging:withVelocity:targetContentOffset: . 根据velocity ,我通过offsetStep增加或减少offsetStep 。 但是我将baseOffset到最小值0和最大值contentSize.width - bounds.size.width

 - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { if (velocity.x < 0) { baseOffset = MAX(0, baseOffset - offsetStep); } else if (velocity.x > 0) { baseOffset = MIN(scrollView.contentSize.width - scrollView.bounds.size.width, baseOffset + offsetStep); } targetContentOffset->x = baseOffset; } 

请注意,我不关心targetContentOffset->x是什么。

这具有与最左边的可见项目的左边缘对齐的效果,直到用户一直滚动到最后一个项目。 此时,它与最右边的可见项的右边缘对齐,直到用户一直向左滚动。 这似乎与App Store应用程序的行为相匹配。

如果这对您不起作用,您可以尝试用以下代码替换最后一行( targetContentOffset->x = baseOffset ):

  dispatch_async(dispatch_get_main_queue(), ^{ [scrollView setContentOffset:CGPointMake(baseOffset, 0) animated:YES]; }); 

这对我也有用。

您可以在此git存储库中找到我的测试应用程序。

您可以打开pagingEnabled ,也可以使用decelerationRate = UIScrollViewDecelerationRateFast结合scrollViewDidEndDraggingscrollViewDidEndDecelerating (这将最小化滚动减慢到一个位置的效果,然后再次动画到另一个位置)。 它可能不是你想要的,但它非常接近。 通过使用animateWithDuration它可以避免最后一次快照的瞬间跳跃。

 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { if (!decelerate) [self snapScrollView:scrollView]; } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { [self snapScrollView:scrollView]; } 

然后,您可以编写snapScrollView ,例如:

 - (void)snapScrollView:(UIScrollView *)scrollView { CGPoint offset = scrollView.contentOffset; if ((offset.x + scrollView.frame.size.width) >= scrollView.contentSize.width) { // no snap needed ... we're at the end of the scrollview return; } // calculate where you want it to snap to offset.x = floorf(offset.x / kIconOffset + 0.5) * kIconOffset; // now snap it to there [UIView animateWithDuration:0.1 animations:^{ scrollView.contentOffset = offset; }]; } 

简单的解决方案,像App Store一样,具有速度或没有速度。 kCellBaseWidth – 单元格的宽度。

 - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { NSInteger index = lrint(targetContentOffset->x/kCellBaseWidth); targetContentOffset->x = index * kCellBaseWidth; } 

为Swift而战 @avdyushin的答案是迄今为止最简单的答案,正如Ben所提到的那样,效果非常好。 虽然我确实添加了@Rob关于滚动视图结束的答案。 总之,这个解决方案似乎完美无缺。

 func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { if ((scrollView.contentOffset.x + scrollView.frame.size.width) >= scrollView.contentSize.width) { // no snap needed ... we're at the end of the scrollview return } let index: CGFloat = CGFloat(lrintf(Float(targetContentOffset.memory.x) / kCellBaseWidth)) targetContentOffset.memory.x = index * kCellBaseWidth } 

只需将minimumLineSpacing添加到kCellBaseWidth