targetContentOffsetForProposedContentOffset: withScrollingVelocity โดยไม่ต้องใช้คลาสย่อย UICollectionViewFlowLayout


100

ฉันมี collectionView ที่เรียบง่ายมากในแอปของฉัน (ภาพขนาดย่อสี่เหลี่ยมเพียงแถวเดียว)

ฉันต้องการสกัดกั้นการเลื่อนเพื่อให้ออฟเซ็ตออกจากภาพเต็มทางด้านซ้ายเสมอ ในขณะนี้มันจะเลื่อนไปที่ใดก็ได้และจะปล่อยภาพที่ถูกตัดออกไป

อย่างไรก็ตามฉันรู้ว่าฉันต้องใช้ฟังก์ชันนี้

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

การทำเช่นนี้ UICollectionViewFlowLayoutแต่ฉันเพียงแค่ใช้มาตรฐาน ฉันไม่ได้คลาสย่อย

มีวิธีใดบ้างในการสกัดกั้นสิ่งนี้โดยไม่ต้องคลาสย่อยUICollectionViewFlowLayout?

ขอบคุณ

คำตอบ:


114

ตกลงคำตอบคือไม่ไม่มีวิธีใดที่จะทำสิ่งนี้ได้หากไม่มีคลาสย่อย UICollectionViewFlowLayout

อย่างไรก็ตามการจัดคลาสย่อยเป็นเรื่องง่ายอย่างไม่น่าเชื่อสำหรับทุกคนที่กำลังอ่านเนื้อหานี้ในอนาคต

ก่อนอื่นฉันตั้งค่าการเรียกคลาสย่อยMyCollectionViewFlowLayoutจากนั้นในตัวสร้างอินเทอร์เฟซฉันเปลี่ยนเค้าโครงมุมมองคอลเลกชันเป็นแบบกำหนดเองและเลือกคลาสย่อยโครงร่างโฟลว์ของฉัน

เนื่องจากคุณทำแบบนี้คุณไม่สามารถระบุขนาดรายการได้ ฯลฯ ... ใน IB ดังนั้นใน MyCollectionViewFlowLayout ฉันมีสิ่งนี้ ...

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

สิ่งนี้ตั้งค่าขนาดทั้งหมดสำหรับฉันและทิศทางการเลื่อน

จากนั้น ...

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

เพื่อให้แน่ใจว่าการเลื่อนจะสิ้นสุดลงโดยมีระยะขอบ 5.0 ที่ขอบด้านซ้ายมือ

นั่นคือทั้งหมดที่ฉันต้องทำ ฉันไม่จำเป็นต้องตั้งค่าเค้าโครงโฟลว์ในโค้ดเลย


1
มันมีประสิทธิภาพมากเมื่อใช้อย่างเหมาะสม คุณเคยดูเซสชัน Collection View จาก WWDC 2012 หรือไม่? พวกเขาน่าจับตามองจริงๆ บางสิ่งที่เหลือเชื่อ
Fogmeister

2
targetContentOffsetForProposedContentOffset:withVelocityไม่ได้ถูกเรียกหาฉันเมื่อฉันเลื่อน เกิดอะไรขึ้น?
fatuhoku

4
@TomSawyer ตั้งค่าอัตราการประกาศของ UICollectionView เป็น UIScrollViewDecelerationRateFast
Clay Ellis

4
@fatuhoku ตรวจสอบให้แน่ใจว่าคุณสมบัติ paginEnabled ของ collectionView ของคุณถูกตั้งค่าเป็นเท็จ
chrs

4
Holy Moly ฉันต้องเลื่อนลงไปเหมือนล้านไมล์เพื่อดูคำตอบนี้ :)
AnBisw

67

การแก้ปัญหาของแดนมีข้อบกพร่อง ไม่สามารถจัดการกับการปัดของผู้ใช้ได้ดี กรณีที่ผู้ใช้ตวัดอย่างรวดเร็วและเลื่อนไม่มากนักจะมีภาพเคลื่อนไหวบกพร่อง

การใช้งานทางเลือกที่ฉันเสนอมีการแบ่งหน้าเหมือนกับที่เสนอไว้ก่อนหน้านี้ แต่จัดการกับการปัดของผู้ใช้ระหว่างหน้า

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

ขอบคุณ! สิ่งนี้ได้ผลเหมือนมีเสน่ห์ ยากที่จะเข้าใจ แต่การไปที่นั่น
Rajiev Timal

ฉันมีข้อผิดพลาดนี้: ไม่สามารถกำหนดให้กับ 'x' ใน 'offerContentOffset'? ใช้ swift? ฉันจะกำหนดให้เป็นค่า x ได้อย่างไร
TomSawyer

@TomSawyer Params เป็น 'let' โดยค่าเริ่มต้น พยายามประกาศฟังก์ชันตามนี้ใน Swift (โดยใช้ var ก่อนหน้าพารามิเตอร์): แทนที่ func targetContentOffsetForProposedContentOffset (var เสนอContentOffset: CGPoint) -> CGPoint
DarthMike

1
คุณไม่สามารถใช้ CGPointMake ได้อย่างรวดเร็ว ฉันใช้สิ่งนี้เป็นการส่วนตัว: "var targetContentOffset: CGPoint ถ้า pannedLessThanAPage && สะบัด {targetContentOffset = CGPoint (x: nextPage * pageWidth (), y: เสนอContentOffset.y);} else {targetContentOffset = CGPoint (x: round (rawPageValue) * pageWidth ( ), y: offerContentOffset.y);} return offerContentOffset "
พล็อ

1
ควรเป็นคำตอบที่เลือก
khunshan

26

คำตอบที่ยอมรับเวอร์ชัน Swift

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

คูปองสำหรับสวิฟท์ 5


เวอร์ชันนี้ใช้งานได้ดีและทำงานได้ดีกับแกน Y ด้วยเช่นกันหากคุณสลับรหัสไปมา
คริส

ส่วนใหญ่ใช้งานได้ดีที่นี่ แต่ถ้าฉันหยุดเลื่อนและยกนิ้วขึ้น (อย่างระมัดระวัง) มันจะไม่เลื่อนไปที่หน้าใด ๆ และหยุดอยู่แค่นั้น
Christian A. Strømmen

@ ChristianA.Strømmenแปลกมันใช้งานได้ดีกับแอพของฉัน
André Abreu

@ AndréAbreuฉันจะวางฟังก์ชันนี้ไว้ที่ไหน
FlowUI SimpleUITesting.com

2
@Jay คุณต้องซับคลาส UICollectionViewLayout หรือคลาสใด ๆ ที่มีคลาสย่อยอยู่แล้ว (เช่น UICollectionViewFlowLayout)
André Abreu

24

นี่คือการใช้งานของฉันในSwift 5สำหรับการเพจตามเซลล์แนวตั้ง :

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

หมายเหตุบางประการ:

  • ไม่ผิดพลาด
  • ตั้งค่าเพจให้เป็นเท็จ ! (มิฉะนั้นจะใช้ไม่ได้)
  • ช่วยให้คุณตั้งค่าการสะบัดของคุณเองได้อย่างง่ายดาย
  • หากบางอย่างยังใช้งานไม่ได้หลังจากลองทำสิ่งนี้ให้ตรวจสอบว่าคุณitemSizeตรงกับขนาดของรายการจริงหรือไม่ซึ่งมักเป็นปัญหาโดยเฉพาะเมื่อใช้collectionView(_:layout:sizeForItemAt:)ให้ใช้ตัวแปรที่กำหนดเองกับ itemSize แทน
  • วิธีนี้จะได้ผลดีที่สุดเมื่อคุณตั้งค่า self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fastนี้ทำงานได้ดีที่สุดเมื่อคุณตั้งค่า

นี่คือแนวนอนเวอร์ชัน (ยังไม่ได้ทดสอบอย่างละเอียดดังนั้นโปรดยกโทษให้กับความผิดพลาด):

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

รหัสนี้อ้างอิงจากรหัสที่ฉันใช้ในโครงการส่วนตัวของฉันคุณสามารถตรวจสอบได้ที่นี่โดยดาวน์โหลดและเรียกใช้เป้าหมายตัวอย่าง


4
คุณคือผู้ช่วยชีวิต! สิ่งสำคัญที่ควรทราบในการตั้งค่าเพจเป็นเท็จ !!! หายไปเหมือน 2 ชั่วโมงในชีวิตของฉันเพื่อแก้ไขหน้าที่ของคุณซึ่งได้ผลแล้ว ...
denis631

@ denis631 ขอโทษด้วย! ฉันควรเพิ่มสิ่งนั้นฉันจะแก้ไขโพสต์เพื่อให้สอดคล้องกับสิ่งนี้! ดีใจที่ได้ผล :)
JoniVR

jesssus ฉันสงสัยว่าทำไมมันถึงไม่ทำงานจนกว่าฉันจะเห็นความคิดเห็นนี้เกี่ยวกับการปิดการใช้งานเพจ ... แน่นอนว่าของฉันถูกตั้งค่าเป็น true
Kam Wo

@JoniVR มันแสดงให้ฉันเห็นข้อผิดพลาดนี้วิธีการไม่ได้แทนที่วิธีการใด ๆ จากซุปเปอร์คลาส
Muju

22

ในขณะที่คำตอบนี้จะช่วยฉันได้ดี แต่ก็มีการสั่นไหวที่เห็นได้ชัดเมื่อคุณปัดอย่างรวดเร็วในระยะทางสั้น ๆ ทำซ้ำบนอุปกรณ์ได้ง่ายกว่ามาก

ฉันพบว่าสิ่งนี้มักเกิดขึ้นเมื่อใดcollectionView.contentOffset.x - proposedContentOffset.xและvelocity.xมีการร้องที่แตกต่างกัน

วิธีแก้ปัญหาของฉันคือตรวจสอบให้แน่ใจว่าความเร็วนั้นproposedContentOffsetมากกว่าcontentOffset.xถ้าความเร็วเป็นบวกและน้อยกว่าหากเป็นลบ อยู่ใน C # แต่ควรแปลเป็น 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);
}

รหัสนี้จะใช้MinContentOffset, MaxContentOffsetและSnapStepที่ควรจะเป็นที่น่ารำคาญสำหรับคุณที่จะกำหนด ในกรณีของฉันพวกเขากลายเป็น

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
นี่ใช้งานได้ดีจริงๆ ฉันแปลงเป็น Objective-C สำหรับผู้ที่สนใจ: gist.github.com/rkeniger/7687301
Rob Keniger

21

หลังจากการทดสอบเป็นเวลานานฉันพบวิธีแก้ปัญหาที่จะจัดกึ่งกลางด้วยความกว้างของเซลล์ที่กำหนดเอง (แต่ละเซลล์มีความกว้างต่างกัน) ซึ่งจะแก้ไขการกะพริบ อย่าลังเลที่จะปรับปรุงสคริปต์

- (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
ทางออกที่ดีที่สุดของพวกเขาทั้งหมดขอบคุณ! นอกจากนี้สำหรับผู้อ่านในอนาคตคุณต้องปิดการเพจเพื่อให้สามารถใช้งานได้
sridvijay

1
หากต้องการจัดแนวจากด้านซ้ายแทนที่จะเป็นเซลล์ที่จัดชิดตรงกลางเราจะเปลี่ยนได้อย่างไร
CyberMew

ไม่แน่ใจว่าฉันเข้าใจถูกต้องหรือไม่ แต่ถ้าคุณต้องการเริ่มรายการที่อยู่ตรงกลางและจัดแนวให้อยู่กึ่งกลางคุณต้องเปลี่ยน contentInset ฉันใช้สิ่งนี้: gist.github.com/pionl/432fc8059dee3b540e38
Pion

ในการจัดตำแหน่งในตำแหน่ง X ของเซลล์ให้อยู่ตรงกลางของมุมมองเพียงแค่ลบ + (layoutAttributes.frame.size.width / 2) ในส่วนความเร็ว
Pion

1
@Jay สวัสดีเพียงสร้างผู้รับมอบสิทธิ์ Flow ที่กำหนดเองและเพิ่มรหัสนี้เข้าไป อย่าลืมกำหนดรูปแบบที่กำหนดเองในปลายปากกาหรือรหัส
Pion

18

อ้างถึงคำตอบนี้โดย Dan Abramovนี่คือเวอร์ชัน 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;
}

หรือส่วนสำคัญที่นี่https://gist.github.com/katopz/8b04c783387f0c345cd9


4
อัปเดตเวอร์ชันนี้สำหรับ Swift 3: gist.github.com/mstubna/beed10327e00310d05f12bf4747266a4
mstubna

1
แดง @mstubna ฉันไปข้างหน้าและคัดลอกข้างต้นอัปเดตเป็น swift 3 เริ่มสร้างส่วนสำคัญที่อัปเดตและกลับมาที่นี่เพื่อรวบรวมบันทึก / ชื่อที่ฉันสังเกตเห็นว่าคุณได้สร้างส่วนสำคัญ 3 อย่างรวดเร็วแล้ว ขอบคุณ! เสียดายที่พลาดไป
VaporwareWolf

18

สำหรับใครที่กำลังมองหาทางออกที่ ...

  • ไม่ต้องดีใจเมื่อผู้ใช้ทำการเลื่อนเร็วสั้น ๆ (กล่าวคือพิจารณาความเร็วในการเลื่อนเป็นบวกและลบ)
  • นำcollectionView.contentInset(และ safeArea บน iPhone X) มาพิจารณาด้วย
  • พิจารณาเฉพาะเซลล์ของสิ่งที่มองเห็นได้ที่จุดเลื่อนเท่านั้น (เพื่อประสิทธิภาพ)
  • ใช้ตัวแปรและความคิดเห็นที่มีชื่อดี
  • คือ Swift 4

โปรดดูด้านล่าง ...

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
ขอบคุณ! เพียงแค่คัดลอกและวางทำงานได้อย่างสมบูรณ์แบบ .. ดีกว่าอย่างที่คาดไว้
Steven B.

1
ทางออกเดียวที่ใช้งานได้จริง งานที่ดี! ขอบคุณ!
LinusGeffarth

1
return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.leftholdersOffsets - sectionInset.left มีปัญหาในบรรทัดนี้
Utku Dalmaz

1
@Dalmaz ขอบคุณสำหรับการแจ้งเตือนฉัน ฉันได้แก้ไขปัญหาแล้ว
Oliver Pearmain

1
ใช่แค่คัดลอกและวางคุณประหยัดเวลาของฉัน
Wei

7

นี่คือโซลูชัน Swift ของฉันในมุมมองคอลเลคชันแบบเลื่อนในแนวนอน เรียบง่ายอ่อนหวานและหลีกเลี่ยงการริบหรี่

  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
  }

คืออะไรitemSize??
Konstantinos Natsios

ขนาดของเซลล์คอลเลกชัน ฟังก์ชันเหล่านี้ใช้เมื่อคลาสย่อย UICollectionViewFlowLayout
Scott Kaiser


1
ฉันชอบวิธีแก้ปัญหานี้ แต่ฉันมีความคิดเห็นสองสามข้อ pageWidth()ควรใช้minimumLineSpacingเนื่องจากเลื่อนในแนวนอน และในกรณีของฉันฉันมีมุมมองคอลเลกชันเพื่อให้เซลล์แรกและครั้งสุดท้ายที่สามารถเป็นศูนย์กลางดังนั้นฉันจะใช้contentInset let xOffset = pageWidth() * index - collectionView.contentInset.left
blwinters

6

ปัญหาเล็กน้อยที่ฉันพบขณะใช้ targetContentOffsetForProposedContentOffset เป็นปัญหาที่เซลล์สุดท้ายไม่ปรับตามจุดใหม่ที่ฉันส่งคืน
ฉันพบว่า CGPoint ที่ฉันส่งคืนมีค่า Y ที่ใหญ่กว่าจึงอนุญาตดังนั้นฉันจึงใช้รหัสต่อไปนี้ในตอนท้ายของการใช้งาน targetContentOffsetForProposedContentOffset:

// 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);

เพื่อให้ชัดเจนขึ้นนี่คือการใช้เลย์เอาต์แบบเต็มของฉันซึ่งเลียนแบบพฤติกรรมการเพจแนวตั้ง:

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

หวังว่าจะช่วยให้ใครบางคนหายปวดหัวได้บ้าง


1
ปัญหาเดียวกันดูเหมือนว่ามุมมองคอลเลกชันจะละเว้นค่าที่ไม่ถูกต้องแทนที่จะปัดเศษให้เป็นขอบเขต
Mike M

6

ฉันต้องการอนุญาตให้ผู้ใช้เลื่อนดูหลาย ๆ หน้า นี่คือเวอร์ชันของฉันtargetContentOffsetForProposedContentOffset(ซึ่งอิงตามคำตอบของ DarthMike) สำหรับเลย์เอาต์แนวตั้ง

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

คำตอบ Fogmeisters ใช้ได้ผลสำหรับฉันเว้นแต่ฉันจะเลื่อนไปที่ท้ายแถว เซลล์ของฉันไม่พอดีกับหน้าจออย่างเป็นระเบียบดังนั้นมันจะเลื่อนไปจนสุดแล้วกระโดดกลับมาพร้อมกับกระตุกเพื่อให้เซลล์สุดท้ายซ้อนทับกับขอบด้านขวาของหน้าจอเสมอ

เพื่อป้องกันไม่ให้เพิ่มบรรทัดของโค้ดต่อไปนี้เมื่อเริ่มต้นเมธอด targetcontentoffset

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

ฉันคิดว่า 320 คือความกว้างมุมมองคอลเลคชันของคุณ :)
Au Ris

ต้องชอบมองย้อนกลับไปที่รหัสเก่า ฉันเดาว่าเลขวิเศษนั่นคือ
Ajaxharg

2

@ André Abreu 's รหัส

เวอร์ชัน 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)
    }
}

ขอบคุณสำหรับสิ่งนั้น! พฤติกรรมที่ดีที่สุดที่คาดหวังขอบคุณมันช่วยได้มาก!
G Clovs

2

สวิฟต์ 4

วิธีที่ง่ายที่สุดสำหรับมุมมองคอลเลกชันที่มีเซลล์ขนาดเดียว (เลื่อนแนวนอน):

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

วิธีแก้ปัญหาที่สั้นกว่า (สมมติว่าคุณกำลังแคชแอตทริบิวต์โครงร่างของคุณ):

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

ในการใส่สิ่งนี้ในบริบท:

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

เพื่อให้แน่ใจว่าใช้งานได้ในเวอร์ชัน Swift (ตอนนี้เร็ว 5) ฉันใช้คำตอบจาก @ André Abreu ฉันจึงเพิ่มข้อมูลเพิ่มเติม:

เมื่อคลาสย่อย UICollectionViewFlowLayout "แทนที่ func AwakenFromNib () {}" จะไม่ทำงาน (ไม่รู้ว่าทำไม) แต่ฉันใช้ "override init () {super.init ()}" แทน

นี่คือรหัสของฉันที่ใส่ในคลาส 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

}

หลังจากคลาสย่อยตรวจสอบให้แน่ใจว่าได้ใส่สิ่งนี้ใน ViewDidLoad ():

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

0

สำหรับผู้ที่กำลังมองหาโซลูชันใน 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)
    }
}

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.