xcode 如何使 UIScrollView 对齐图标(如 App Store:功能)

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15961099/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-15 03:11:33  来源:igfitidea点击:

How to make a UIScrollView snap to icons (like App Store: Feature)

iphoneiosxcodecocoauiscrollview

提问by fbernardo

What I want to get is the same behaviour that this scroll view has:

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

App Store feature scroll view (left)

App Store 功能滚动视图(左)

I know that this is using HTML and not the native API, but I'm trying to implement it as a UIKit component.

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

Now, to the behaviour I'm looking for:

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

  • Notice that it's a paged scroll view, but the "page size" is less than the view's width.
  • When you scroll it from left to right each page "snap" to the left-most item.
  • When you scroll it from the right end to the left it "snaps" to the right-most item.
  • 请注意,这是一个分页滚动视图,但“页面大小”小于视图的宽度。
  • 当您从左到右滚动它时,每个页面都会“对齐”到最左侧的项目。
  • 当您从右端向左滚动它时,它会“对齐”到最右侧的项目。

The same page but now right-to-left:

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

App Store feature scroll view (right)

App Store 功能滚动视图(右)

What I've tried:

我试过的:

  • I've tried making the scroll view smaller than it's super view and overriding hitTest, and that got me that left-to-right behaviour.
  • I've tried implementing scrollViewWillEndDragging:withVelocity:targetContentOffset: and setting the targetContentOffset I want but since I can't change the velocity it just scrolls too slowly or too fast.
  • I've tried implementing scrollViewDidEndDecelerating: and then animating to the correct offset but the scroll view first stops then moves, it doesn't look natural.
  • I've tried implementing scrollViewDidEndDragging:willDecelerate: and then animating to the correct offset but the scroll view "jumps" and does not animate correctly.
  • 我已经尝试使滚动视图小于它的超级视图并覆盖 hitTest,这让我得到了从左到右的行为。
  • 我试过实现 scrollViewWillEndDragging:withVelocity:targetContentOffset: 并设置我想要的 targetContentOffset 但由于我无法改变速度,它只是滚动太慢或太快。
  • 我试过实现 scrollViewDidEndDecelerating: 然后动画到正确的偏移量但滚动视图首先停止然后移动,它看起来不自然。
  • 我试过实现 scrollViewDidEndDragging:willDecelerate: 然后动画到正确的偏移但滚动视图“跳跃”并且没有正确动画。

I'm out of ideas.

我没主意了。

Thanks!

谢谢!

Update:

更新:

I ended up using Rob Mayoff's method, it looks clean. I changed it so it would work when the velocity is 0, for example when a user drags, stops and releases the finger.

我最终使用了 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;
}

The only problem with this solution is that swiping the scroll view doesn't produce the "bounce" effect. It feels "stuck".

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

回答by rob mayoff

Setting scrollView.decelerationRate = UIScrollViewDecelerationRateFast, combined with implementing scrollViewWillEndDragging:withVelocity:targetContentOffset:, seems to work for me using a collection view.

Setting scrollView.decelerationRate = UIScrollViewDecelerationRateFast,结合 implementation scrollViewWillEndDragging:withVelocity:targetContentOffset:,似乎对我使用集合视图工作。

First, I give myself some instance variables:

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

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

In viewDidLoad, I set the view's decelerationRate:

在 中viewDidLoad,我设置了视图的decelerationRate

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

I need offsetStepto be the size of an integral number of items that fit in the view's on-screen bounds. I compute it in viewDidLayoutSubviews:

我需要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);
}

I need baseOffsetto the be the X offset of the view before scrolling starts. I initialize it in viewDidAppear::

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

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

Then I need to force the view to scroll in steps of offsetStep. I do that in scrollViewWillEndDragging:withVelocity:targetContentOffset:. Depending on the velocity, I increase or decrease baseOffsetby offsetStep. But I clamp baseOffsetto a minimum of 0 and a maximum of the contentSize.width - bounds.size.width.

然后我需要强制视图以offsetStep. 我在scrollViewWillEndDragging:withVelocity:targetContentOffset:. 视情况而定velocity,我增加或减少baseOffsetoffsetStep。但我钳制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;
}

Note that I don't care what targetContentOffset->xcomes in as.

请注意,我不在乎targetContentOffset->x进来的是什么。

This has the effect of aligning to the left edge of the leftmost visible item, until the user scrolls all the way to the last item. At that point it aligns to the right edge of the rightmost visible item, until the user scroll all the way to the left. This seems to match the behavior of the App Store app.

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

If that doesn't work for you, you can try replacing the last line (targetContentOffset->x = baseOffset) with this:

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

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

That also works for me.

这也适用于我。

You can find my test app in this git repository.

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

回答by Rob

You can either turn on pagingEnabled, or use decelerationRate = UIScrollViewDecelerationRateFastcombined with scrollViewDidEndDraggingand scrollViewDidEndDecelerating(which will minimize the effect of slowing scrolling to one location and then animating again to another location). It's probably not precisely what you want, but it's pretty close. And by using animateWithDurationit avoids the instantaneous jumping of that final snap.

您可以打开pagingEnabled,也可以decelerationRate = UIScrollViewDecelerationRateFastscrollViewDidEndDragging和结合使用scrollViewDidEndDecelerating(这将最大程度地减少缓慢滚动到一个位置然后再次动画到另一个位置的影响)。这可能不是您想要的,但非常接近。并通过使用animateWithDuration它避免了最后一个快照的瞬间跳跃。

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

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self snapScrollView:scrollView];
}

You can then write a snapScrollView, such as:

然后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;
                     }];
}

回答by avdyushin

Simple solution, works like App Store, with velocity or without it. kCellBaseWidth— width of cell.

简单的解决方案,像 App Store 一样工作,无论是否有速度。kCellBaseWidth— 单元格的宽度。

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

回答by slider

Chiming in for Swift. @avdyushin's answer is by far the simplest and, as Ben mentioned, works very well. Although I did add a piece from @Rob's answer regarding the end of the scrollview. Together, this solution seems to work perfectly.

为 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

}

Just add your minimumLineSpacingto kCellBaseWidth, and voila.

只需将您的添加minimumLineSpacingkCellBaseWidth,瞧。