【问题标题】:UIScrollView custom paging sizeUIScrollView 自定义分页大小
【发布时间】:2011-10-12 09:34:45
【问题描述】:

UIScrollView 中的分页是一个很棒的功能,我在这里需要将分页设置为更小的距离,例如我希望我的 UIScrollView 的页面大小小于 UIScrollView 框架宽度。 谢谢

【问题讨论】:

标签: iphone objective-c ipad uiscrollview


【解决方案1】:

您可以使用UIScrollView 委托方法。将您的类设置为滚动视图的委托,然后实现以下内容:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    CGFloat kMaxIndex = 23;
    CGFloat targetX = scrollView.contentOffset.x + velocity.x * 60.0;
    CGFloat targetIndex = 0.0;
    if (velocity.x > 0) {
        targetIndex = ceil(targetX / (kCellWidth + kCellSpacing));
    } else if (velocity.x == 0) {
        targetIndex = round(targetX / (kCellWidth + kCellSpacing));
    } else if (velocity.x < 0) {
        targetIndex = floor(targetX / (kCellWidth + kCellSpacing));
    }
    if (targetIndex < 0)
        targetIndex = 0;
    if (targetIndex > kMaxIndex)
        targetIndex = kMaxIndex;
    targetContentOffset->x = targetIndex * (kCellWidth + kCellSpacing);
    //scrollView.decelerationRate = UIScrollViewDecelerationRateFast;//uncomment this for faster paging
}

速度参数是必要的,以确保滚动感觉自然,并且在手指仍在移动时触摸结束时不会突然结束。单元格宽度和单元格间距是视图中页面之间的页面宽度和间距。在这种情况下,我使用的是UICollectionView

【讨论】:

  • 如果你想避免滚动视图在进行非常小的滑动时跳转到它的初始位置,你可以使用这个:if (velocity.x &gt; 0) { targetIndex = ceil(targetX / (self.cardViewWidth + kCardSpacing)); } else { targetIndex = floor(targetX / (self.cardViewWidth + kCardSpacing)); }
  • @lucious,我想通了:我必须将 scrollView.pagingEnabled 设置为 NO :)
  • 为什么targetX表达式最后乘以60?这是我唯一无法理解的部分。这个神奇的数字从何而来?
  • @KoCMoHaBTa 根据文档,“速度以点/毫秒为单位”,因此这通过添加滚动视图在接下来的 60 毫秒内移动的距离来考虑拖动力。更强大的拖动将进一步滚动。
  • 别忘了玩 scrollView.decelerationRate 来获得你想要的感觉 - 我必须将它设置为 UIScrollViewDecelerationRateFast 才能感觉自然
【解决方案2】:
  1. 将滚动视图大小更改为所需的页面大小
  2. 设置你的scroll.clipsToBounds = NO
  3. 创建一个 UIView 子类(例如 HackClipView)并覆盖 hitTest:withEvent: 方法

    -(UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event
    {     
        UIView* child = [super hitTest:point withEvent:event]; 
        if (child == self && self.subviews.count > 0)  
        {
            return self.subviews[0];
        }
        return child;
    }
    
  4. 设置HackClipView.clipsToBounds = YES

  5. 将你的 scrollView 放在这个 HackClipView 中(你想要的总滚动大小)

详情请见this answer

更新: 如 lucius answer 中所述,您现在可以实现 UIScollViewDelegate 协议并使用 - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset 方法。因为targetContentOffset 是一个指针。使用此方法不能保证滚动视图页面的结果相同,因为用户可以一次滚动多个页面。但是将descelerationRate 设置为fast几乎给你同样的结果

【讨论】:

  • 这是一个糟糕的答案。如果您使滚动视图更小,那么可触摸区域也会缩小到您设置的框架。该问题专门询问如何处理“小于 UIScrollView 框架宽度的页面”。
  • @Joshua,不,通过覆盖 superview 上的 hitTest 方法,您可以重定向 scrollView 子级的触摸。
  • @Francescu @Joshua 或在滚动视图子类中覆盖 pointInside:withEvent:
  • 不适用于 UITableView 和 UICollectionView,导致超出原始边界的单元格(通常“隐藏”但由于 clipToBounds=NO 而可见)将从超级视图中删除。
  • 尝试了 UICollectionView 并且来自可见区域之外的单元格被隐藏/重用,除了滚动方向的下一个单元格。未能成功诱骗它缓存更多单元格。此外,拖动触摸只有在小滚动视图内开始时才有效
【解决方案3】:

您应该禁用分页并将UIPanGestureRecognizer 添加到滚动视图并自己处理分页。

- (void)viewDidLoad {

    [super viewDidLoad];
    CGRect viewRect = self.view.bounds; // View controller's view bounds
    theScrollView = [[UIScrollView alloc] initWithFrame:viewRect]; 

    theScrollView.scrollsToTop      = NO;
    theScrollView.pagingEnabled         = NO;
    theScrollView.delaysContentTouches  = NO;
    theScrollView.delegate = self;

    [self.view addSubview:theScrollView];

    UIPanGestureRecognizer * peter = [[[UIPanGestureRecognizer alloc] initWithTarget:self  
                                                                              action:@selector(handlePan:)]
                                       autorelease]; 
    [theScrollView addGestureRecognizer:peter]; 

}

-(void)handlePan:(UIPanGestureRecognizer*)recognizer{

    switch (recognizer.state) {
    case UIGestureRecognizerStateBegan:{
        // panStart and startPoint are instance vars for the viewContainer 
        panStart = theScrollView.contentOffset;
        startPoint = [recognizer locationInView:theScrollView]; 
        
        
        break;
    }
    case UIGestureRecognizerStateChanged:{
                    
        CGPoint newPoint = [recognizer locationInView:theScrollView];
        CGFloat delta = startPoint.x - newPoint.x;
        if ( abs(delta) > 2)
            theScrollView.contentOffset = CGPointMake( theScrollView.contentOffset.x + delta, 0); 
        
        CGFloat moveDelta = panStart.x - theScrollView.contentOffset.x;                               
        

        // current witdh should hold the currently displayed page/view in theScrollView
        if ( abs(moveDelta) > (currentWidth * 0.40)){
            panStart = theScrollView.contentOffset;
            startPoint = newPoint;
            
            //NSLog(@"delta is bigger"); 
            if ( moveDelta < 0 )
                [self incrementPageNumber]; // you should implement this method and present the next view
            else 
                [self decrementPageNumber]; // you should implement this method and present the previous view
   
            recognizer.enabled = NO; // disable further event until view change finish
                 
        }
        
        break; 
    }
        
    case UIGestureRecognizerStateEnded:
    case UIGestureRecognizerStateCancelled:

        recognizer.enabled = YES; 
        [self showDocumentPage:currentPage]; 
        
        break;
        
        
    default:
        break;
    }
}

【讨论】:

  • 我投了赞成票,因为这是一个不同的功能解决方案,并且为滚动行为提供了更多的可定制性。
  • 在大多数情况下是非常糟糕的主意
【解决方案4】:

简化重用的 Swift 4.1 解决方案:

/// Protocol that simplifies custom page size configuration for UIScrollView.
/// Sadly, can not be done better due to protocol extensions limitations - https://stackoverflow.com/questions/39487168/non-objc-method-does-not-satisfy-optional-requirement-of-objc-protocol
/// - note: Set `.decelerationRate` to `UIScrollViewDecelerationRateFast` for a fancy scrolling animation.
protocol ScrollViewCustomHorizontalPageSize: UIScrollViewDelegate {
    /// Custom page size
    var pageSize: CGFloat { get }

    /// Helper method to get current page fraction
    func getCurrentPage(scrollView: UIScrollView) -> CGFloat

    /// Helper method to get targetContentOffset. Usage:
    ///
    ///     func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    ///         targetContentOffset.pointee.x = getTargetContentOffset(scrollView: scrollView, velocity: velocity)
    ///     }
    func getTargetContentOffset(scrollView: UIScrollView, velocity: CGPoint) -> CGFloat

    /// Must be implemented. See `getTargetContentOffset` for more info.
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
}

extension ScrollViewCustomHorizontalPageSize {
    func getCurrentPage(scrollView: UIScrollView) -> CGFloat {
        return (scrollView.contentOffset.x + scrollView.contentInset.left) / pageSize
    }

    func getTargetContentOffset(scrollView: UIScrollView, velocity: CGPoint) -> CGFloat {
        let targetX: CGFloat = scrollView.contentOffset.x + velocity.x * 60.0

        var targetIndex = (targetX + scrollView.contentInset.left) / pageSize
        let maxOffsetX = scrollView.contentSize.width - scrollView.bounds.width + scrollView.contentInset.right
        let maxIndex = (maxOffsetX + scrollView.contentInset.left) / pageSize
        if velocity.x > 0 {
            targetIndex = ceil(targetIndex)
        } else if velocity.x < 0 {
            targetIndex = floor(targetIndex)
        } else {
            let (maxFloorIndex, lastInterval) = modf(maxIndex)
            if targetIndex > maxFloorIndex {
                if targetIndex >= lastInterval / 2 + maxFloorIndex {
                    targetIndex = maxIndex
                } else {
                    targetIndex = maxFloorIndex
                }
            } else {
                targetIndex = round(targetIndex)
            }
        }

        if targetIndex < 0 {
            targetIndex = 0
        }

        var offsetX = targetIndex * pageSize - scrollView.contentInset.left
        offsetX = min(offsetX, maxOffsetX)

        return offsetX
    }
}

只需在 UIScrollView/UITableView/UICollectionView 委托中遵守ScrollViewCustomPageSize 协议即可,例如:

extension MyCollectionViewController: ScrollViewCustomPageSize {
    var pageSize: CGFloat {
        return 200
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        targetContentOffset.pointee.x = getTargetContentOffset(scrollView: scrollView, velocity: velocity)
    }
}

对于花哨的滚动,我还建议设置collectionView.decelerationRate = UIScrollViewDecelerationRateFast

【讨论】:

  • 有没有办法将每次操作的滚动锁定到一页?使用以下方法,我有时可以滚动两页,但每个方向只需要一个。有时现在:0 - 1 - 3 - 4,想要:0 - 1 - 2 - 3 - 4
  • @mkz 嗨,您可以在 getTargetContentOffset 方法中计算 offsetX 之前钳制 targetIndex。我没有测试,但类似的东西应该可以工作:targetIndex = max(targetIndex, floor(getCurrentPage(scrollView: scrollView)); targetIndex = min(targetIndex, ceil(getCurrentPage(scrollView: scrollView))
  • 方法被触发,但宽度没有限制。你有例子吗?
  • @bruno 您可以查看github.com/APUtils/APExtensions/tree/… 有一个自定义页面大小示例
【解决方案5】:

设置contentOffset -(void)scrollViewDidScroll:(UIScrollView *)scrollView 方法。

另请参阅UIScrollViewDelegate refernces

【讨论】:

  • 它对我没有帮助,我想要一些动画
【解决方案6】:

我遇到了同样的问题,所以我制作了一个自定义 UIScrollView。 它现在在 Github 上可用,因为当我搜索时,我没有找到任何这样的解决方案。享受! https://github.com/MartinMetselaar/MMCPSScrollView

MMCPSScrollView* scrollView = [[MMCPSScrollView alloc] initWithFrame:self.view.bounds];
[scrollView setType:MMCPSScrollVertical];
[scrollView setPageHeight:250];
[scrollView setPageSize:2];
[self.view addSubview:scrollView];

如果您对此组件有任何其他问题,请尽管提问。

【讨论】:

    【解决方案7】:

    添加手势识别器或其他子视图等是愚蠢的。只需将滚动视图的委托设置为以下内容:

    // This is for a vertical scrolling scroll view. 
    // Let's say you want it to snap to every 160 pixels :    
    
    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
    {     
         int y = scrollView.contentOffset.y;
         int yOff = y % 160;
         if(yOff < 80)
              y -= yOff;
         else
              y += 160 - yOff;
    
         [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, y) animated:YES];
    }
    
    // This is for a horizontal scrolling scroll view.
    // Let's say you want the same, to snap to every 160 pixels :
    
    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
    {
         int x = scrollView.contentOffset.x;
         int xOff = x % 160;
         if(xOff < 80)
              x -= xOff;
         else
              x += 160 - xOff;
    
         [scrollView setContentOffset:CGPointMake(x, scrollView.contentOffset.y) animated:YES];
    }
    

    【讨论】:

      【解决方案8】:

      Swift 4.1,iOS11+:

      func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
          targetContentOffset.pointee = CGPoint(
              x: round(targetContentOffset.pointee.x / pageWidth) * pageWidth,
              y: targetContentOffset.pointee.y
          )
      }
      

      【讨论】:

        【解决方案9】:

        不久前我遇到了同样的问题。我的方法是在滚动视图中添加第二个UIScrollView。所以你可以切换到页面。在该页面上,它似乎比页面大于屏幕。我希望它也适用于您的情况。 ;-)

        桑德罗·迈耶

        【讨论】:

        • 这对我没有帮助,我已经嵌套了 2 个 UIScrollViews
        【解决方案10】:

        这对我来说似乎效果更好:

        UIScrollView Custom Paging

        在这里,他们将滚动视图(保持其分页功能)作为子视图添加到 ExtendedTouchView 或 UIVIew 的子类,并覆盖命中测试方法

        - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        if ([self pointInside:point withEvent:event]) {
            if ([[self subviews] count] > 0) {
                //force return of first child, if exists
                return [[self subviews] objectAtIndex:0];
            } else {
                return self;
            }
        }
        return nil;
        }
        

        这正是我想要的,只需最少的代码和令人头疼的问题。

        【讨论】:

          【解决方案11】:

          最简单的方法是添加这段代码

          scrollView.clipsToBounds = false
          scrollView.removeGestureRecognizer(scrollView.panGestureRecognizer)
          view.addGestureRecognizer(scrollView.panGestureRecognizer)
          

          【讨论】:

          • 我认为您应该详细说明一下。为什么你的代码有效?
          猜你喜欢
          • 2014-11-06
          • 2017-05-20
          • 1970-01-01
          • 1970-01-01
          • 2012-03-11
          • 1970-01-01
          • 1970-01-01
          • 2020-10-15
          • 2018-07-18
          相关资源
          最近更新 更多