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

时间:2013-04-12 00:24:15

标签: iphone ios xcode cocoa uiscrollview

我想要获得的是与此滚动视图相同的行为:

App Store feature scroll view (left)

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

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

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

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

App Store feature scroll view (right)

我尝试了什么:

  • 我尝试使滚动视图小于它的超级视图并覆盖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;
}

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

4 个答案:

答案 0 :(得分:14)

设置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:中这样做。根据{{​​1}},我会velocity增加或减少baseOffset。但我将offsetStep限制在最小值0和最大值baseOffset

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; } 是什么意思。

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

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

targetContentOffset->x = baseOffset

这对我也有用。

您可以找到我的测试应用in this git repository

答案 1 :(得分:12)

您可以启用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;
                     }];
}

答案 2 :(得分:5)

简单的解决方案,像App Store一样,有速度或无速度。 kCellBaseWidth - 细胞宽度。

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

答案 3 :(得分:2)

为Swift做准备。到目前为止,@avdyushin的答案是最简单的,而且正如Ben所说,它的效果非常好。虽然我确实添加了@Rob关于滚动视图结束的答案。总之,这个解决方案似乎完美无缺。

func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    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即可。