开发者

UIScrollView custom paging size

paging开发者_运维知识库 in UIScrollView is a great feature, what I need here is to set the paging to a smaller distance, for example I want my UIScrollView to page less size that the UIScrollView frame width. Thanks


There is a UIScrollView delegate method you can use. Set your class as the scroll view's delegate, and then implement the following:

- (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
}

The velocity parameter is necessary to make sure the scrolling feels natural and doesn't end abruptly when a touch ends with your finger still moving. The cell width and cell spacing are the page width and spacing between pages in your view. In this case, I'm using a UICollectionView.


  1. Change your scrollView size to the page size you want
  2. Set your scroll.clipsToBounds = NO
  3. Create a UIView subclass (e.g HackClipView) and override the hitTest:withEvent: method

    -(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. Set the HackClipView.clipsToBounds = YES

  5. Put your scrollView in this HackClipView (with the total scrolling size you want)

See this answer for more details

Update: As stated in lucius answer you can now implement the UIScollViewDelegate protocol and use the - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset method. As the targetContentOffset is a pointer. Using this method will not guarantee you the same result with scroll view pages as the user can scroll through many pages at once. But setting the descelerationRate to fast will almost give you the same result


You should disable paging and add a UIPanGestureRecognizer to your scroll view and handle the paging yourself.

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


Swift 4.1 solution that simplifies reusing:

/// 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
    }
}

Just conform to ScrollViewCustomPageSize protocol in your UIScrollView/UITableView/UICollectionView delegate and you are done, e.g.:

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)
    }
}

For a fancy scrolling I also recommend to set collectionView.decelerationRate = UIScrollViewDecelerationRateFast


Set the contentOffset in -(void)scrollViewDidScroll:(UIScrollView *)scrollView method.

Also refer to UIScrollViewDelegate refernces


I had the same problem so I have made a custom UIScrollView. It's available on Github now because when I searched I didn't find any solutions like this. Enjoy! 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];

If you have any further questions about this component, just ask.


Adding gesture recognizers or other subviews and so on is silly. Just set the delegate for the scroll view an imlement on of the below :

// 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];
}


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
    )
}


I had the same problem short ago. My aproach was to add a second UIScrollView to the scrollview. So you can switch to the page. On that page it seems than if the page is bigger than the screen. I hope it works also in your situation. ;-)

Sandro Meier


This seemed to work a lot better for me:

UIScrollView Custom Paging

Here they are adding the scrollview (keeping it's paging niceness) as a subview to an ExtendedTouchView or subclass of UIVIew and overwriting the hit test method

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

This did exactly whatI wanted with minimal code and headache.


The easiest way is to add this code

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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜