targetContentOffsetForProposedContentOffset: withScrollingVelocity mà không cần phân lớp UICollectionViewFlowLayout


99

Tôi có một collectionView rất đơn giản trong ứng dụng của mình (chỉ một hàng hình ảnh thu nhỏ hình vuông).

Tôi muốn chặn việc cuộn để phần bù luôn để lại hình ảnh đầy đủ ở phía bên trái. Tại thời điểm này, nó cuộn đến bất cứ nơi nào và sẽ để lại những hình ảnh bị cắt.

Dù sao, tôi biết tôi cần phải sử dụng hàm

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

để làm điều này nhưng tôi chỉ sử dụng một tiêu chuẩn UICollectionViewFlowLayout. Tôi không phân loại nó.

Có cách nào để chặn điều này mà không cần phân lớp UICollectionViewFlowLayoutkhông?

Cảm ơn

Câu trả lời:


113

OK, câu trả lời là không, không có cách nào để thực hiện việc này mà không phân lớp UICollectionViewFlowLayout.

Tuy nhiên, việc phân loại nó trở nên vô cùng dễ dàng đối với những ai đang đọc nó trong tương lai.

Đầu tiên, tôi thiết lập lệnh gọi lớp con MyCollectionViewFlowLayoutvà sau đó trong trình tạo giao diện, tôi đã thay đổi bố cục dạng xem bộ sưu tập thành Tùy chỉnh và chọn lớp con bố cục luồng của mình.

Bởi vì bạn đang làm theo cách này, bạn không thể chỉ định kích thước mục, v.v. trong IB nên trong MyCollectionViewFlowLayout.m, tôi có cái này ...

- (void)awakeFromNib
{
    self.itemSize = CGSizeMake(75.0, 75.0);
    self.minimumInteritemSpacing = 10.0;
    self.minimumLineSpacing = 10.0;
    self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    self.sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0);
}

Điều này thiết lập tất cả các kích thước cho tôi và hướng cuộn.

Sau đó ...

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    CGFloat offsetAdjustment = MAXFLOAT;
    CGFloat horizontalOffset = proposedContentOffset.x + 5;

    CGRect targetRect = CGRectMake(proposedContentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);

    NSArray *array = [super layoutAttributesForElementsInRect:targetRect];

    for (UICollectionViewLayoutAttributes *layoutAttributes in array) {
        CGFloat itemOffset = layoutAttributes.frame.origin.x;
        if (ABS(itemOffset - horizontalOffset) < ABS(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset;
        }
    }

    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

Điều này đảm bảo rằng quá trình cuộn kết thúc với lề 5,0 ở cạnh bên trái.

Đó là tất cả những gì tôi cần làm. Tôi không cần thiết lập bố cục luồng trong mã.


1
Nó thực sự mạnh mẽ khi được sử dụng đúng cách. Bạn đã xem phần Xem bộ sưu tập từ WWDC 2012 chưa? Họ thực sự đáng xem. Một số thứ đáng kinh ngạc.
Fogmeister

2
targetContentOffsetForProposedContentOffset:withVelocitykhông được gọi cho tôi khi tôi cuộn. Chuyện gì vậy?
fatuhoku

4
@TomSawyer đặt tốc độ khai báo của UICollectionView thành UIScrollViewDecelerationRateFast.
Clay Ellis

3
@fatuhoku đảm bảo rằng thuộc tính paginEnabled của collectionView của bạn được đặt thành false
chrs

4
Thánh Moly, tôi đã phải di chuyển xuống giống như một triệu dặm để xem câu trả lời này. :)
AnBisw

67

Giải pháp của Dan là thiếu sót. Nó không xử lý tốt việc nhấp nháy của người dùng. Các trường hợp khi người dùng lướt nhanh và cuộn không di chuyển nhiều, hoạt ảnh bị trục trặc.

Việc triển khai thay thế được đề xuất của tôi có cùng phân trang như đã đề xuất trước đây, nhưng xử lý việc người dùng lướt qua giữa các trang.

 #pragma mark - Pagination
 - (CGFloat)pageWidth {
     return self.itemSize.width + self.minimumLineSpacing;
 }

 - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
 {           
        CGFloat rawPageValue = self.collectionView.contentOffset.x / self.pageWidth;
        CGFloat currentPage = (velocity.x > 0.0) ? floor(rawPageValue) : ceil(rawPageValue);
        CGFloat nextPage = (velocity.x > 0.0) ? ceil(rawPageValue) : floor(rawPageValue);

        BOOL pannedLessThanAPage = fabs(1 + currentPage - rawPageValue) > 0.5;
        BOOL flicked = fabs(velocity.x) > [self flickVelocity];
        if (pannedLessThanAPage && flicked) {
            proposedContentOffset.x = nextPage * self.pageWidth;
        } else {
            proposedContentOffset.x = round(rawPageValue) * self.pageWidth;
        }

        return proposedContentOffset;
 }

 - (CGFloat)flickVelocity {
     return 0.3;
 }

Cảm ơn bạn! Điều này làm việc như một sự quyến rũ. Một chút khó hiểu nhưng đạt được điều đó.
Rajiev Timal

Tôi đang gặp lỗi này: Không thể gán cho 'x' trong 'suggestContentOffset'? Sử dụng yến? làm thế nào tôi có thể gán cho giá trị x?
TomSawyer

@TomSawyer Params là 'let' theo mặc định. Cố gắng khai báo hàm như thế này trong Swift (sử dụng var trước param): ghi đè func targetContentOffsetForProposedContentOffset (var suggestContentOffset: CGPoint) -> CGPoint
DarthMike 29/09

1
Bạn không thể sử dụng CGPointMake nhanh chóng. Cá nhân tôi đã sử dụng cái này: "var targetContentOffset: CGPoint if pannedLessThanAPage && flicked {targetContentOffset = CGPoint (x: nextPage * pageWidth (), y: suggestContentOffset.y);} else {targetContentOffset = CGPoint (x: round (rawPageValue) * pageWidth ( ), y: suggestContentOffset.y);} return suggestContentOffset "
Plot

1
Nó phải là câu trả lời được chọn.
khunshan

26

Phiên bản Swift của câu trả lời được chấp nhận.

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var offsetAdjustment = CGFloat.greatestFiniteMagnitude
    let horizontalOffset = proposedContentOffset.x
    let targetRect = CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: self.collectionView!.bounds.size)

    for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
        let itemOffset = layoutAttributes.frame.origin.x
        if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset
        }
    }

    return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}    

Có hiệu lực cho Swift 5 .


Phiên bản này hoạt động tốt và nó cũng hoạt động tốt cho trục Y nếu bạn hoán đổi mã xung quanh.
Chris

Hầu hết hoạt động tuyệt vời ở đây. Nhưng nếu tôi ngừng cuộn và nhấc ngón tay lên (cẩn thận), nó sẽ không cuộn đến bất kỳ trang nào và chỉ dừng lại ở đó.
Christian A. Strømmen,

@ ChristianA.Strømmen Kỳ lạ, Nó hoạt động tốt với ứng dụng của tôi.
André Abreu

@ AndréAbreu Tôi đặt chức năng này ở đâu?
FlowUI. SimpleUITesting.com

2
@Jay Bạn cần phân lớp UICollectionViewLayout hoặc bất kỳ lớp nào đã phân lớp nó (ví dụ: UICollectionViewFlowLayout).
André Abreu

24

Đây là cách triển khai của tôi trong Swift 5 cho phân trang dựa trên ô dọc :

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    guard let collectionView = self.collectionView else {
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    }

    // Page height used for estimating and calculating paging.
    let pageHeight = self.itemSize.height + self.minimumLineSpacing

    // Make an estimation of the current page position.
    let approximatePage = collectionView.contentOffset.y/pageHeight

    // Determine the current page based on velocity.
    let currentPage = velocity.y == 0 ? round(approximatePage) : (velocity.y < 0.0 ? floor(approximatePage) : ceil(approximatePage))

    // Create custom flickVelocity.
    let flickVelocity = velocity.y * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - collectionView.contentInset.top

    return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}

Một số lưu ý:

  • Không trục trặc
  • ĐẶT PAGING THÀNH FALSE ! (nếu không điều này sẽ không hoạt động)
  • Cho phép bạn thiết lập vận tốc của riêng mình một cách dễ dàng.
  • Nếu cái gì đó vẫn không hoạt động sau khi thử cách này, hãy kiểm tra xem bạn có itemSizethực sự phù hợp với kích thước của mặt hàng hay không vì đó thường là một vấn đề, đặc biệt là khi sử dụngcollectionView(_:layout:sizeForItemAt:) , hãy sử dụng biến tùy chỉnh với itemSize để thay thế.
  • Điều này hoạt động tốt nhất khi bạn thiết lập self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Đây là phiên bản nằm ngang (chưa được kiểm tra kỹ lưỡng nên xin mọi người bỏ qua cho mọi sai sót)

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

    guard let collectionView = self.collectionView else {
        let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        return latestOffset
    }

    // Page width used for estimating and calculating paging.
    let pageWidth = self.itemSize.width + self.minimumInteritemSpacing

    // Make an estimation of the current page position.
    let approximatePage = collectionView.contentOffset.x/pageWidth

    // Determine the current page based on velocity.
    let currentPage = velocity.x == 0 ? round(approximatePage) : (velocity.x < 0.0 ? floor(approximatePage) : ceil(approximatePage))

    // Create custom flickVelocity.
    let flickVelocity = velocity.x * 0.3

    // Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
    let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)

    // Calculate newHorizontalOffset.
    let newHorizontalOffset = ((currentPage + flickedPages) * pageWidth) - collectionView.contentInset.left

    return CGPoint(x: newHorizontalOffset, y: proposedContentOffset.y)
}

Mã này dựa trên mã tôi sử dụng trong dự án cá nhân của mình, bạn có thể kiểm tra tại đây bằng cách tải xuống và chạy mục tiêu Ví dụ.


4
Bạn là vị cứu tinh cuộc đời! Điều quan trọng cần lưu ý để ĐẶT PAGING TO FALSE !!! Mất như 2 giờ trong đời tôi sửa chữa chức năng của bạn, mà đã hoạt động ...
denis631

@ denis631 Tôi rất xin lỗi! Tôi nên thêm điều đó, tôi sẽ chỉnh sửa bài đăng để phản ánh điều này! Rất vui vì nó đã hoạt động :)
JoniVR

jesssus, tôi đã tự hỏi tại sao điều này không hoạt động cho đến khi tôi thấy nhận xét này về việc vô hiệu hóa phân trang ... tất nhiên tôi đã được thiết lập là true
Kam Wo

@JoniVR Nó cho tôi thấy lỗi này Phương thức không ghi đè bất kỳ phương thức nào từ lớp cha của nó
Muju

22

Trong khi câu trả lời này này đã giúp ích rất nhiều cho tôi, nhưng có một hiện tượng nhấp nháy đáng chú ý khi bạn vuốt nhanh trên một khoảng cách nhỏ. Việc tái tạo nó trên thiết bị dễ dàng hơn nhiều.

Tôi thấy rằng điều này luôn xảy ra khi collectionView.contentOffset.x - proposedContentOffset.xvelocity.xcó những bài hát khác nhau.

Giải pháp của tôi là đảm bảo rằng proposedContentOffsetnhiều hơn contentOffset.xnếu vận tốc là dương và ít hơn nếu nó là âm. Nó trong C # nhưng sẽ khá đơn giản để dịch sang Objective C:

public override PointF TargetContentOffset (PointF proposedContentOffset, PointF scrollingVelocity)
{
    /* Determine closest edge */

    float offSetAdjustment = float.MaxValue;
    float horizontalCenter = (float) (proposedContentOffset.X + (this.CollectionView.Bounds.Size.Width / 2.0));

    RectangleF targetRect = new RectangleF (proposedContentOffset.X, 0.0f, this.CollectionView.Bounds.Size.Width, this.CollectionView.Bounds.Size.Height);
    var array = base.LayoutAttributesForElementsInRect (targetRect);

    foreach (var layoutAttributes in array) {
        float itemHorizontalCenter = layoutAttributes.Center.X;
        if (Math.Abs (itemHorizontalCenter - horizontalCenter) < Math.Abs (offSetAdjustment)) {
            offSetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
    }

    float nextOffset = proposedContentOffset.X + offSetAdjustment;

    /*
     * ... unless we end up having positive speed
     * while moving left or negative speed while moving right.
     * This will cause flicker so we resort to finding next page
     * in the direction of velocity and use it.
     */

    do {
        proposedContentOffset.X = nextOffset;

        float deltaX = proposedContentOffset.X - CollectionView.ContentOffset.X;
        float velX = scrollingVelocity.X;

        // If their signs are same, or if either is zero, go ahead
        if (Math.Sign (deltaX) * Math.Sign (velX) != -1)
            break;

        // Otherwise, look for the closest page in the right direction
        nextOffset += Math.Sign (scrollingVelocity.X) * SnapStep;
    } while (IsValidOffset (nextOffset));

    return proposedContentOffset;
}

bool IsValidOffset (float offset)
{
    return (offset >= MinContentOffset && offset <= MaxContentOffset);
}

Mã này được sử dụng MinContentOffset, MaxContentOffsetSnapStepđó phải là tầm thường để bạn có thể xác định. Trong trường hợp của tôi, họ hóa ra là

float MinContentOffset {
    get { return -CollectionView.ContentInset.Left; }
}

float MaxContentOffset {
    get { return MinContentOffset + CollectionView.ContentSize.Width - ItemSize.Width; }
}

float SnapStep {
    get { return ItemSize.Width + MinimumLineSpacing; }
}

7
Điều này thực sự hoạt động tốt. Tôi chuyển đổi nó để Objective-C cho những người quan tâm: gist.github.com/rkeniger/7687301
Rob Keniger

21

Sau thời gian dài thử nghiệm, tôi đã tìm ra giải pháp để gắn vào giữa với chiều rộng ô tùy chỉnh (mỗi ô có chiều rộng khác nhau) để khắc phục hiện tượng nhấp nháy. Hãy tự do cải thiện kịch bản.

- (CGPoint) targetContentOffsetForProposedContentOffset: (CGPoint) proposedContentOffset withScrollingVelocity: (CGPoint)velocity
{
    CGFloat offSetAdjustment = MAXFLOAT;
    CGFloat horizontalCenter = (CGFloat) (proposedContentOffset.x + (self.collectionView.bounds.size.width / 2.0));

    //setting fastPaging property to NO allows to stop at page on screen (I have pages lees, than self.collectionView.bounds.size.width)
    CGRect targetRect = CGRectMake(self.fastPaging ? proposedContentOffset.x : self.collectionView.contentOffset.x, 
                                   0.0,
                                   self.collectionView.bounds.size.width,
                                   self.collectionView.bounds.size.height);

    NSArray *attributes = [self layoutAttributesForElementsInRect:targetRect];
    NSPredicate *cellAttributesPredicate = [NSPredicate predicateWithBlock: ^BOOL(UICollectionViewLayoutAttributes * _Nonnull evaluatedObject,
                                                                             NSDictionary<NSString *,id> * _Nullable bindings) 
    {
        return (evaluatedObject.representedElementCategory == UICollectionElementCategoryCell); 
    }];        

    NSArray *cellAttributes = [attributes filteredArrayUsingPredicate: cellAttributesPredicate];

    UICollectionViewLayoutAttributes *currentAttributes;

    for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
    {
        CGFloat itemHorizontalCenter = layoutAttributes.center.x;
        if (ABS(itemHorizontalCenter - horizontalCenter) < ABS(offSetAdjustment))
        {
            currentAttributes   = layoutAttributes;
            offSetAdjustment    = itemHorizontalCenter - horizontalCenter;
        }
    }

    CGFloat nextOffset          = proposedContentOffset.x + offSetAdjustment;

    proposedContentOffset.x     = nextOffset;
    CGFloat deltaX              = proposedContentOffset.x - self.collectionView.contentOffset.x;
    CGFloat velX                = velocity.x;

    // detection form  gist.github.com/rkeniger/7687301
    // based on http://stackoverflow.com/a/14291208/740949
    if (fabs(deltaX) <= FLT_EPSILON || fabs(velX) <= FLT_EPSILON || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) 
    {

    } 
    else if (velocity.x > 0.0) 
    {
       // revert the array to get the cells from the right side, fixes not correct center on different size in some usecases
        NSArray *revertedArray = [[array reverseObjectEnumerator] allObjects];

        BOOL found = YES;
        float proposedX = 0.0;

        for (UICollectionViewLayoutAttributes *layoutAttributes in revertedArray)
        {
            if(layoutAttributes.representedElementCategory == UICollectionElementCategoryCell)
            {
                CGFloat itemHorizontalCenter = layoutAttributes.center.x;
                if (itemHorizontalCenter > proposedContentOffset.x) {
                     found = YES;
                     proposedX = nextOffset + (currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2);
                } else {
                     break;
                }
            }
        }

       // dont set on unfound element
        if (found) {
            proposedContentOffset.x = proposedX;
        }
    } 
    else if (velocity.x < 0.0) 
    {
        for (UICollectionViewLayoutAttributes *layoutAttributes in cellAttributes)
        {
            CGFloat itemHorizontalCenter = layoutAttributes.center.x;
            if (itemHorizontalCenter > proposedContentOffset.x) 
            {
                proposedContentOffset.x = nextOffset - ((currentAttributes.frame.size.width / 2) + (layoutAttributes.frame.size.width / 2));
                break;
            }
        }
    }

    proposedContentOffset.y = 0.0;

    return proposedContentOffset;
}

10
Giải pháp tốt nhất của tất cả, cảm ơn! Ngoài ra đối với bất kỳ độc giả nào trong tương lai, bạn phải tắt phân trang để tính năng này hoạt động.
sridvijay

1
Nếu một người muốn căn chỉnh nó từ bên trái, thay vì ô được căn ngay ở giữa, chúng ta sẽ thay đổi nó như thế nào?
CyberMew

Không chắc liệu tôi có hiểu đúng hay không, nhưng nếu bạn muốn bắt đầu các mục ở trung tâm và căn chỉnh nó vào giữa, bạn cần thay đổi contentInset. Tôi sử dụng cái này: gist.github.com/pionl/432fc8059dee3b540e38
Pion

Để căn chỉnh ở vị trí X của ô ở giữa chế độ xem, chỉ cần xóa + (layoutAttributes.frame.size.width / 2) trong phần vận tốc.
Pion

1
@Jay Xin chào, chỉ cần tạo một đại biểu Flow tùy chỉnh và thêm mã này vào nó. Đừng quên đặt bố cục tùy chỉnh trong nib hoặc mã.
Pion

18

tham khảo câu trả lời này của Dan Abramov, đây là phiên bản Swift

    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var _proposedContentOffset = CGPoint(x: proposedContentOffset.x, y: proposedContentOffset.y)
    var offSetAdjustment: CGFloat = CGFloat.max
    let horizontalCenter: CGFloat = CGFloat(proposedContentOffset.x + (self.collectionView!.bounds.size.width / 2.0))

    let targetRect = CGRect(x: proposedContentOffset.x, y: 0.0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)

    let array: [UICollectionViewLayoutAttributes] = self.layoutAttributesForElementsInRect(targetRect)! as [UICollectionViewLayoutAttributes]
    for layoutAttributes: UICollectionViewLayoutAttributes in array {
        if (layoutAttributes.representedElementCategory == UICollectionElementCategory.Cell) {
            let itemHorizontalCenter: CGFloat = layoutAttributes.center.x
            if (abs(itemHorizontalCenter - horizontalCenter) < abs(offSetAdjustment)) {
                offSetAdjustment = itemHorizontalCenter - horizontalCenter
            }
        }
    }

    var nextOffset: CGFloat = proposedContentOffset.x + offSetAdjustment

    repeat {
        _proposedContentOffset.x = nextOffset
        let deltaX = proposedContentOffset.x - self.collectionView!.contentOffset.x
        let velX = velocity.x

        if (deltaX == 0.0 || velX == 0 || (velX > 0.0 && deltaX > 0.0) || (velX < 0.0 && deltaX < 0.0)) {
            break
        }

        if (velocity.x > 0.0) {
            nextOffset = nextOffset + self.snapStep()
        } else if (velocity.x < 0.0) {
            nextOffset = nextOffset - self.snapStep()
        }
    } while self.isValidOffset(nextOffset)

    _proposedContentOffset.y = 0.0

    return _proposedContentOffset
}

func isValidOffset(offset: CGFloat) -> Bool {
    return (offset >= CGFloat(self.minContentOffset()) && offset <= CGFloat(self.maxContentOffset()))
}

func minContentOffset() -> CGFloat {
    return -CGFloat(self.collectionView!.contentInset.left)
}

func maxContentOffset() -> CGFloat {
    return CGFloat(self.minContentOffset() + self.collectionView!.contentSize.width - self.itemSize.width)
}

func snapStep() -> CGFloat {
    return self.itemSize.width + self.minimumLineSpacing;
}

hoặc ý chính tại đây https://gist.github.com/katopz/8b04c783387f0c345cd9


4
Phiên bản cập nhật này cho Swift 3: gist.github.com/mstubna/beed10327e00310d05f12bf4747266a4
mstubna

1
Dang it @mstubna, tôi đã tiếp tục và sao chép ở trên, cập nhật nó lên swift 3, bắt đầu tạo ý chính cập nhật và quay lại đây để thu thập ghi chú / tiêu đề tại thời điểm đó, tôi nhận thấy rằng bạn đã thực hiện swift 3 ý. Cảm ơn! Thật tệ là tôi đã bỏ lỡ nó.
VaporwareWolf

16

Đối với bất kỳ ai đang tìm kiếm một giải pháp ...

  • KHÔNG ĐƯỢC GẮN khi người dùng thực hiện cuộn nhanh ngắn (tức là nó xem xét vận tốc cuộn dương và âm)
  • lấy cái collectionView.contentInset (và safeArea trên iPhone X) vào xem xét
  • chỉ xem xét các ô có thể nhìn thấy tại điểm cuộn (đối với hiệu suất)
  • sử dụng các biến và nhận xét được đặt tên tốt
  • là Swift 4

thì vui lòng xem bên dưới ...

public class CarouselCollectionViewLayout: UICollectionViewFlowLayout {

    override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

        guard let collectionView = collectionView else {
            return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
        }

        // Identify the layoutAttributes of cells in the vicinity of where the scroll view will come to rest
        let targetRect = CGRect(origin: proposedContentOffset, size: collectionView.bounds.size)
        let visibleCellsLayoutAttributes = layoutAttributesForElements(in: targetRect)

        // Translate those cell layoutAttributes into potential (candidate) scrollView offsets
        let candidateOffsets: [CGFloat]? = visibleCellsLayoutAttributes?.map({ cellLayoutAttributes in
            if #available(iOS 11.0, *) {
                return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left - sectionInset.left
            } else {
                return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - sectionInset.left
            }
        })

        // Now we need to work out which one of the candidate offsets is the best one
        let bestCandidateOffset: CGFloat

        if velocity.x > 0 {
            // If the scroll velocity was POSITIVE, then only consider cells/offsets to the RIGHT of the proposedContentOffset.x
            // Of the cells/offsets to the right, the NEAREST is the `bestCandidate`
            // If there is no nearestCandidateOffsetToLeft then we default to the RIGHT-MOST (last) of ALL the candidate cells/offsets
            //      (this handles the scenario where the user has scrolled beyond the last cell)
            let candidateOffsetsToRight = candidateOffsets?.toRight(ofProposedOffset: proposedContentOffset.x)
            let nearestCandidateOffsetToRight = candidateOffsetsToRight?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffsetToRight ?? candidateOffsets?.last ?? proposedContentOffset.x
        }
        else if velocity.x < 0 {
            // If the scroll velocity was NEGATIVE, then only consider cells/offsets to the LEFT of the proposedContentOffset.x
            // Of the cells/offsets to the left, the NEAREST is the `bestCandidate`
            // If there is no nearestCandidateOffsetToLeft then we default to the LEFT-MOST (first) of ALL the candidate cells/offsets
            //      (this handles the scenario where the user has scrolled beyond the first cell)
            let candidateOffsetsToLeft = candidateOffsets?.toLeft(ofProposedOffset: proposedContentOffset.x)
            let nearestCandidateOffsetToLeft = candidateOffsetsToLeft?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffsetToLeft ?? candidateOffsets?.first ?? proposedContentOffset.x
        }
        else {
            // If the scroll velocity was ZERO we consider all `candidate` cells (regarless of whether they are to the left OR right of the proposedContentOffset.x)
            // The cell/offset that is the NEAREST is the `bestCandidate`
            let nearestCandidateOffset = candidateOffsets?.nearest(toProposedOffset: proposedContentOffset.x)
            bestCandidateOffset = nearestCandidateOffset ??  proposedContentOffset.x
        }

        return CGPoint(x: bestCandidateOffset, y: proposedContentOffset.y)
    }

}

fileprivate extension Sequence where Iterator.Element == CGFloat {

    func toLeft(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {

        return filter() { candidateOffset in
            return candidateOffset < proposedOffset
        }
    }

    func toRight(ofProposedOffset proposedOffset: CGFloat) -> [CGFloat] {

        return filter() { candidateOffset in
            return candidateOffset > proposedOffset
        }
    }

    func nearest(toProposedOffset proposedOffset: CGFloat) -> CGFloat? {

        guard let firstCandidateOffset = first(where: { _ in true }) else {
            // If there are no elements in the Sequence, return nil
            return nil
        }

        return reduce(firstCandidateOffset) { (bestCandidateOffset: CGFloat, candidateOffset: CGFloat) -> CGFloat in

            let candidateOffsetDistanceFromProposed = fabs(candidateOffset - proposedOffset)
            let bestCandidateOffsetDistancFromProposed = fabs(bestCandidateOffset - proposedOffset)

            if candidateOffsetDistanceFromProposed < bestCandidateOffsetDistancFromProposed {
                return candidateOffset
            }

            return bestCandidateOffset
        }
    }
}

1
Cảm ơn! vừa được sao chép & dán, hoạt động hoàn hảo .. tốt hơn như mong đợi.
Steven B.

1
Một và duy nhất giải pháp thực sự hoạt động. Công việc tốt! Cảm ơn!
LinusGeffarth

1
trở cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left candidateOffsets - sectionInset.left có một vấn đề trong dòng này
Utku Dalmaz

1
@Dalmaz cảm ơn vì đã thông báo cho tôi. Tôi đã khắc phục sự cố ngay bây giờ.
Oliver Pearmain

1
Đúng, chỉ cần sao chép và dán, bạn tiết kiệm thời gian của tôi.
Wei

7

Đây là giải pháp Swift của tôi trên chế độ xem bộ sưu tập cuộn theo chiều ngang. Nó đơn giản, ngọt ngào và tránh mọi nhấp nháy.

  override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else { return proposedContentOffset }

    let currentXOffset = collectionView.contentOffset.x
    let nextXOffset = proposedContentOffset.x
    let maxIndex = ceil(currentXOffset / pageWidth())
    let minIndex = floor(currentXOffset / pageWidth())

    var index: CGFloat = 0

    if nextXOffset > currentXOffset {
      index = maxIndex
    } else {
      index = minIndex
    }

    let xOffset = pageWidth() * index
    let point = CGPointMake(xOffset, 0)

    return point
  }

  func pageWidth() -> CGFloat {
    return itemSize.width + minimumInteritemSpacing
  }

là gì itemSize??
Konstantinos Natsios

Kích thước của các ô thu thập. Các chức năng này được sử dụng khi phân lớp UICollectionViewFlowLayout.
Scott Kaiser


1
Tôi thích giải pháp này, nhưng tôi có một vài nhận xét. pageWidth()nên sử dụng minimumLineSpacingvì nó cuộn theo chiều ngang. Và trong trường hợp của tôi, tôi có contentInsetchế độ xem bộ sưu tập để ô đầu tiên và ô cuối cùng có thể được căn giữa, vì vậy tôi sử dụng let xOffset = pageWidth() * index - collectionView.contentInset.left.
blwinters

6

một vấn đề nhỏ mà tôi gặp phải khi sử dụng targetContentOffsetForProposedContentOffset là sự cố với ô cuối cùng không điều chỉnh theo điểm mới mà tôi đã trả lại.
Tôi phát hiện ra rằng CGPoint mà tôi trả về có giá trị Y lớn hơn sau đó được phép, vì vậy tôi đã sử dụng mã sau ở cuối triển khai targetContentOffsetForProposedContentOffset của mình:

// if the calculated y is bigger then the maximum possible y we adjust accordingly
CGFloat contentHeight = self.collectionViewContentSize.height;
CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
CGFloat maxY = contentHeight - collectionViewHeight;
if (newY > maxY)
{
    newY = maxY;
}

return CGPointMake(0, newY);

chỉ để làm cho nó rõ ràng hơn đây là triển khai bố cục đầy đủ của tôi chỉ bắt chước hành vi phân trang dọc:

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    return [self targetContentOffsetForProposedContentOffset:proposedContentOffset];
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
    CGFloat heightOfPage = self.itemSize.height;
    CGFloat heightOfSpacing = self.minimumLineSpacing;

    CGFloat numOfPage = lround(proposedContentOffset.y / (heightOfPage + heightOfSpacing));
    CGFloat newY = numOfPage * (heightOfPage + heightOfSpacing);

    // if the calculated y is bigger then the maximum possible y we adjust accordingly
    CGFloat contentHeight = self.collectionViewContentSize.height;
    CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
    CGFloat maxY = contentHeight - collectionViewHeight;
    if (newY > maxY)
    {
        newY = maxY;
    }

    return CGPointMake(0, newY);
}

hy vọng điều này sẽ giúp ai đó tiết kiệm thời gian và cơn đau đầu


1
Cùng một vấn đề, có vẻ như chế độ xem bộ sưu tập sẽ bỏ qua các giá trị không hợp lệ thay vì làm tròn chúng về giới hạn của nó.
Mike M

6

Tôi muốn cho phép người dùng lướt qua một số trang. Vì vậy, đây là phiên bản của tôi targetContentOffsetForProposedContentOffset(dựa trên câu trả lời DarthMike) cho bố cục dọc .

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
    CGFloat approximatePage = self.collectionView.contentOffset.y / self.pageHeight;
    CGFloat currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage);

    NSInteger flickedPages = ceil(velocity.y / self.flickVelocity);

    if (flickedPages) {
        proposedContentOffset.y = (currentPage + flickedPages) * self.pageHeight;
    } else {
        proposedContentOffset.y = currentPage * self.pageHeight;
    }

    return proposedContentOffset;
}

- (CGFloat)pageHeight {
    return self.itemSize.height + self.minimumLineSpacing;
}

- (CGFloat)flickVelocity {
    return 1.2;
}

4

Câu trả lời của Fogmeisters phù hợp với tôi trừ khi tôi cuộn đến cuối hàng. Các ô của tôi không vừa khít trên màn hình nên nó sẽ cuộn đến cuối và giật lùi để ô cuối cùng luôn chồng lên cạnh phải của màn hình.

Để ngăn chặn điều này, hãy thêm dòng mã sau vào đầu phương thức targetcontentoffset

if(proposedContentOffset.x>self.collectionViewContentSize.width-320-self.sectionInset.right)
    return proposedContentOffset;

Tôi cho rằng 320 là chiều rộng chế độ xem bộ sưu tập của bạn :)
Au Ris

Phải thích nhìn lại mã cũ. Tôi đoán đó là con số kỳ diệu.
Ajaxharg

2

@ André Abreu 's Mã

Phiên bản Swift3

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        var offsetAdjustment = CGFloat.greatestFiniteMagnitude
        let horizontalOffset = proposedContentOffset.x
        let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
        for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
            let itemOffset = layoutAttributes.frame.origin.x
            if abs(itemOffset - horizontalOffset) < abs(offsetAdjustment){
                offsetAdjustment = itemOffset - horizontalOffset
            }
        }
        return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    }
}

Cảm ơn vì điều đó! Hành vi tốt nhất được mong đợi Cảm ơn nó Giúp rất nhiều!
G Clovs

2

Swift 4

Giải pháp đơn giản nhất cho chế độ xem bộ sưu tập với các ô có một kích thước (cuộn ngang):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else { return proposedContentOffset }

    // Calculate width of your page
    let pageWidth = calculatedPageWidth()

    // Calculate proposed page
    let proposedPage = round(proposedContentOffset.x / pageWidth)

    // Adjust necessary offset
    let xOffset = pageWidth * proposedPage - collectionView.contentInset.left

    return CGPoint(x: xOffset, y: 0)
}

func calculatedPageWidth() -> CGFloat {
    return itemSize.width + minimumInteritemSpacing
}

2

Một giải pháp ngắn hơn (giả sử bạn đang lưu vào bộ nhớ đệm các thuộc tính bố cục của mình):

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
    let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
    return CGPoint(x: targetLayoutAttributes.frame.minX - horizontalPadding, y: 0)
}

Để đặt điều này trong ngữ cảnh:

class Layout : UICollectionViewLayout {
    private var cache: [UICollectionViewLayoutAttributes] = []
    private static let horizontalPadding: CGFloat = 16
    private static let interItemSpacing: CGFloat = 8

    override func prepare() {
        let (itemWidth, itemHeight) = (collectionView!.bounds.width - 2 * Layout.horizontalPadding, collectionView!.bounds.height)
        cache.removeAll()
        let count = collectionView!.numberOfItems(inSection: 0)
        var x: CGFloat = Layout.horizontalPadding
        for item in (0..<count) {
            let indexPath = IndexPath(item: item, section: 0)
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = CGRect(x: x, y: 0, width: itemWidth, height: itemHeight)
            cache.append(attributes)
            x += itemWidth + Layout.interItemSpacing
        }
    }

    override var collectionViewContentSize: CGSize {
        let width: CGFloat
        if let maxX = cache.last?.frame.maxX {
            width = maxX + Layout.horizontalPadding
        } else {
            width = collectionView!.width
        }
        return CGSize(width: width, height: collectionView!.height)
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cache.first { $0.indexPath == indexPath }
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return cache.filter { $0.frame.intersects(rect) }
    }

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        let proposedEndFrame = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView!.bounds.width, height: collectionView!.bounds.height)
        let targetLayoutAttributes = cache.max { $0.frame.intersection(proposedEndFrame).width < $1.frame.intersection(proposedEndFrame).width }!
        return CGPoint(x: targetLayoutAttributes.frame.minX - Layout.horizontalPadding, y: 0)
    }
}

1

Để đảm bảo nó hoạt động trong phiên bản Swift (swift 5 bây giờ), tôi đã sử dụng câu trả lời từ @ André Abreu, tôi bổ sung thêm một số thông tin:

Khi phân lớp UICollectionViewFlowLayout, "ghi đè func awafromNib () {}" không hoạt động (không biết tại sao). Thay vào đó, tôi đã sử dụng "override init () {super.init ()}"

Đây là mã của tôi được đặt trong lớp SubclassFlowLayout: UICollectionViewFlowLayout {}:

let padding: CGFloat = 16
override init() {
    super.init()
    self.minimumLineSpacing = padding
    self.minimumInteritemSpacing = 2
    self.scrollDirection = .horizontal
    self.sectionInset = UIEdgeInsets(top: 0, left: padding, bottom: 0, right: 100) //right = "should set for footer" (Horizental)

}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
    var offsetAdjustment = CGFloat.greatestFiniteMagnitude
    let leftInset = padding
    let horizontalOffset = proposedContentOffset.x + leftInset // leftInset is for "where you want the item stop on the left"
    let targetRect = CGRect(origin: CGPoint(x: proposedContentOffset.x, y: 0), size: self.collectionView!.bounds.size)

    for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
        let itemOffset = layoutAttributes.frame.origin.x
        if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
            offsetAdjustment = itemOffset - horizontalOffset
        }
    }

    let targetPoint = CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    return targetPoint

}

Sau khi phân lớp con, hãy đảm bảo đặt điều này vào ViewDidLoad ():

customCollectionView.collectionViewLayout = SubclassFlowLayout()
customCollectionView.isPagingEnabled = false
customCollectionView.decelerationRate = .fast //-> this for scrollView speed

0

Đối với những người đang tìm kiếm một giải pháp trong Swift:

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
    private let collectionViewHeight: CGFloat = 200.0
    private let screenWidth: CGFloat = UIScreen.mainScreen().bounds.width

    override func awakeFromNib() {
        super.awakeFromNib()

        self.itemSize = CGSize(width: [InsertItemWidthHere], height: [InsertItemHeightHere])
        self.minimumInteritemSpacing = [InsertItemSpacingHere]
        self.scrollDirection = .Horizontal
        let inset = (self.screenWidth - CGFloat(self.itemSize.width)) / 2
        self.collectionView?.contentInset = UIEdgeInsets(top: 0,
                                                         left: inset,
                                                         bottom: 0,
                                                         right: inset)
    }

    override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        var offsetAdjustment = CGFloat.max
        let horizontalOffset = proposedContentOffset.x + ((self.screenWidth - self.itemSize.width) / 2)

        let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.screenWidth, height: self.collectionViewHeight)
        var array = super.layoutAttributesForElementsInRect(targetRect)

        for layoutAttributes in array! {
            let itemOffset = layoutAttributes.frame.origin.x
            if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
                offsetAdjustment = itemOffset - horizontalOffset
            }
        }

        return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    }
}

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.