如何使UIScrollView对象图标(如App Store:Feature)
我想要得到的是此滚动视图具有的相同行为:
我知道这是使用HTML而不是本机API,但我正在尝试将其实现为UIKit组件。
现在,我正在寻找的行为:
- 请注意,它是一个分页滚动视图,但“页面大小”小于视图的宽度。
- 当您从左向右滚动时,每个页面“捕捉”到最左侧的项目。
- 当您从右端向左滚动时,它会“捕捉”到最右侧的项目。
同一页面,但现在从右到左:
我尝试过的:
- 我已经尝试使滚动视图小于它的超级视图并覆盖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
结合scrollViewDidEndDragging
和scrollViewDidEndDecelerating
(这将最小化滚动减慢到一个位置的效果,然后再次动画到另一个位置)。 它可能不是你想要的,但它非常接近。 通过使用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
。