ios 以小于帧大小的增量分页 UIScrollView

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1677085/
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-08-30 16:35:26  来源:igfitidea点击:

Paging UIScrollView in increments smaller than frame size

iosuiscrollviewscroll-paging

提问by Henning

I have a scroll view that is the width of the screen but only about 70 pixels high. It contains many 50 x 50 icons (with space around them) that I want the user to be able to choose from. But I always want the scroll view to behave in a paged manner, always stopping with an icon in the exact center.

我有一个滚动视图,它是屏幕的宽度,但只有大约 70 像素高。它包含许多我希望用户能够选择的 50 x 50 图标(它们周围有空格)。但我总是希望滚动视图以分页方式运行,始终在正中心显示一个图标。

If the icons were the width of the screen this wouldn't be a problem because the UIScrollView's paging would take care of it. But because my little icons are much less than the content size, it doesn't work.

如果图标是屏幕的宽度,这不会有问题,因为 UIScrollView 的分页会处理它。但是因为我的小图标远小于内容大小,所以它不起作用。

I've seen this behavior before in an app call AllRecipes. I just don't know how to do it.

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

How do I get paging on a per-icon sized basis to work?

如何在每个图标大小的基础上分页工作?

回答by Ben Gottlieb

Try making your scrollview less than the size of the screen (width-wise), but uncheck the "Clip Subviews" checkbox in IB. Then, overlay a transparent, userInteractionEnabled = NO view on top of it (at full width), which overrides hitTest:withEvent: to return your scroll view. That should give you what you're looking for. See this answerfor more details.

尝试使您的滚动视图小于屏幕的大小(宽度方向),但取消选中 IB 中的“剪辑子视图”复选框。然后,在其顶部覆盖一个透明的 userInteractionEnabled = NO 视图(全宽),它会覆盖 hitTest:withEvent: 以返回您的滚动视图。那应该给你你正在寻找的东西。有关更多详细信息,请参阅此答案

回答by Split

There is also another solution wich is probably a little bit better than overlaying scroll view with another view and overriding hitTest.

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

You can subclass UIScrollView and override its pointInside. Then scroll view can respond for touches outside its frame. Of course the rest is the same.

您可以继承 UIScrollView 并覆盖其 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

回答by Angel G. Olloqui

I see a lot of solutions, but they are very complex. A much easier wayto have small pages but still keep all area scrollable, is to make the scroll smaller and move the scrollView.panGestureRecognizerto your parent view. These are the steps:

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

  1. Reduce your scrollView sizeScrollView size is smaller than parent

  2. Make sure your scroll view is paginated and does not clip subview enter image description here

  3. In code, move the scrollview pan gesture to the parent container view that is full width:

  1. 减少您的滚动视图大小ScrollView 大小小于父级

  2. 确保您的滚动视图已分页并且不会剪辑子视图 在此处输入图片说明

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

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

回答by colinta

The accepted answer is very good, but it will only work for the UIScrollViewclass, and none of its descendants. For instance if you have lots of views and convert to a UICollectionView, you will not be able to use this method, because the collection view will removeviews that it thinks are "not visible" (so even though they aren't clipped, they will disappear).

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

The comment about that mentions scrollViewWillEndDragging:withVelocity:targetContentOffset:is, in my opinion, the correct answer.

scrollViewWillEndDragging:withVelocity:targetContentOffset:在我看来,有关提及的评论是正确的答案。

What you can do is, inside this delegate method you calculate the current page/index. Then you decide whether the velocity and target offset merit a "next page" movement. You can get pretty close to the pagingEnabledbehavior.

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

note:I'm usually a RubyMotion dev these days, so someone please proof this Obj-C code for correctness. Sorry for the mix of camelCase and snake_case, I copy&pasted much of this code.

注意:这些天我通常是 RubyMotion 开发人员,所以请有人证明这个 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;
    }
} 

回答by vish

- (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 is the width you want your page to be. kPageOffset is if you don't want the cells to be left aligned (i.e. if you want them to be center aligned, set this to half the width of your cell). Otherwise, it should be zero.

kPageWidth 是您希望页面的宽度。kPageOffset 是如果您不希望单元格左对齐(即,如果您希望它们居中对齐,请将其设置为单元格宽度的一半)。否则,它应该为零。

This will also only allow scrolling one page at a time.

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

回答by Noah Witherspoon

Take a look at the -scrollView:didEndDragging:willDecelerate:method on UIScrollViewDelegate. Something like:

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

- (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)];
}

It isn't perfect—last I tried this code I got some ugly behavior (jumping, as I recall) when returning from a rubber-banded scroll. You might be able to avoid that by simply setting the scroll view's bouncesproperty to NO.

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

回答by Jacob Persson

Since I don't seem to be permitted to comment yet I'll add my comments to Noah's answer here.

由于我似乎还不允许发表评论,因此我将在此处将我的评论添加到 Noah 的答案中。

I've successfully achieved this by the method that Noah Witherspoon described. I worked around the jumping behavior by simply not calling the setContentOffset:method when the scrollview is past its edges.

我已经通过 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;

             ...
         }

I also found that I needed implement the -scrollViewWillBeginDecelerating:method in UIScrollViewDelegateto catch all cases.

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

回答by tyler

I tried out the solution above that overlayed a transparent view with pointInside:withEvent: overridden. This worked pretty well for me, but broke down for certain cases - see my comment. I ended up just implementing the paging myself with a combination of scrollViewDidScroll to track the current page index and scrollViewWillEndDragging:withVelocity:targetContentOffset and scrollViewDidEndDragging:willDecelerate to snap to the appropriate page. Note, the will-end method is only available iOS5+, but is pretty sweet for targeting a particular offset if the velocity != 0. Specifically, you can tell the caller where you want the scroll view to land with animation if there's velocity in a particular direction.

我尝试了上面的解决方案,该解决方案用 pointInside:withEvent: 覆盖覆盖了一个透明视图。这对我来说效果很好,但在某些情况下会崩溃 - 请参阅我的评论。我最终只是使用 scrollViewDidScroll 的组合来实现分页,以跟踪当前页面索引和 scrollViewWillEndDragging:withVelocity:targetContentOffset 和 scrollViewDidEndDragging:willDecelerate 以捕捉到适当的页面。请注意,will-end 方法仅适用于 iOS5+,但如果速度为 != 0,则它非常适合定位特定的偏移量。具体来说,如果速度为特定的方向。

回答by TimWhiting

This is the only real solution to the problem.

这是问题的唯一真正解决方案。

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

        }

    }


}

回答by DawnSong

Here is my answer. In my example, a collectionViewwhich has a section header is the scrollView that we want to make it has custom isPagingEnabledeffect, and cell's height is a constant value.

这是我的答案。在我的例子中,collectionView有一个section header的a是我们想让它有自定义isPagingEnabled效果的scrollView,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
}