UICollectionViewFlowLayout을 서브 클래 싱하지 않고 targetContentOffsetForProposedContentOffset : withScrollingVelocity


99

내 앱에 매우 간단한 collectionView가 있습니다 (정사각형 썸네일 이미지의 단일 행).

오프셋이 항상 왼쪽에 전체 이미지를 남기도록 스크롤을 가로 채고 싶습니다. 현재 어디로 든 스크롤하여 잘린 이미지를 남깁니다.

어쨌든이 기능을 사용해야한다는 것을 알고 있습니다.

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

이것을하기 위해 나는 단지 표준을 사용하고 있습니다 UICollectionViewFlowLayout. 나는 그것을 하위 분류하지 않습니다.

서브 클래 싱없이 이것을 가로 챌 수있는 방법이 UICollectionViewFlowLayout있습니까?

감사

답변:


113

좋아요, 대답은 아니오입니다. UICollectionViewFlowLayout을 서브 클래 싱하지 않고는이를 수행 할 수있는 방법이 없습니다.

그러나 서브 클래 싱은 미래에 이것을 읽는 모든 사람에게 매우 쉽습니다.

먼저 서브 클래스 호출을 설정 MyCollectionViewFlowLayout한 다음 인터페이스 빌더에서 컬렉션 뷰 레이아웃을 Custom으로 변경하고 플로우 레이아웃 서브 클래스를 선택했습니다.

이런 식으로 수행하기 때문에 IB에서 항목 크기 등을 지정할 수 없으므로 MyCollectionViewFlowLayout.m 에이 있습니다 ...

- (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
제대로 사용하면 정말 강력합니다. WWDC 2012의 Collection View 세션을 보셨습니까? 그들은 정말 볼만한 가치가 있습니다. 놀라운 것들.
— Fogmeister

2
targetContentOffsetForProposedContentOffset:withVelocity스크롤 할 때 나를 부르지 않습니다. 무슨 일이야?
— fatuhoku

4
@TomSawyer는 UICollectionView의 선언 속도를 UIScrollViewDecelerationRateFast로 설정합니다.
— 클레이 엘리스

3
@fatuhoku 확실히 당신의 collectionView의 paginEnabled 속성이 false로 설정되어 있는지 확인
— CHRS

4
Holy Moly, 나는이 대답을보기 위해 백만 마일처럼 스크롤을 내려야했다. :)
— AnBisw

67

Dan의 솔루션에 결함이 있습니다. 사용자 플릭을 잘 처리하지 못합니다. 사용자가 빠르게 플릭하고 스크롤하는 경우 너무 많이 움직이지 않고 애니메이션 결함이 있습니다.

내가 제안한 대체 구현은 이전에 제안 된 것과 동일한 페이지 매김을 갖지만 사용자가 페이지 사이를 긋는 것을 처리합니다.

 #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

이 오류가 있습니다 : 'proposedContentOffset'에서 'x'에 할당 할 수 없습니까? 신속한 사용? x 값에 어떻게 할당 할 수 있습니까?
— TomSawyer 2014 년

@TomSawyer 매개 변수는 기본적으로 'let'입니다. Swift에서 다음과 같이 함수를 선언하십시오 (var before param 사용) : override func targetContentOffsetForProposedContentOffset (var
— suggestContentOffset

1
신속하게 CGPointMake를 사용할 수 없습니다. 나는 개인적으로 이것을 사용했다 : "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 "
— 플롯

1
선택한 답변이어야합니다.
— khunshan

26

수락 된 답변의 신속한 버전.

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

Swift 5에 유효합니다 .


이 버전은 훌륭하게 작동하며 코드를 교체하면 Y 축에서도 잘 작동합니다.
— 크리스

대부분 여기서 잘 작동합니다. 그러나 스크롤을 멈추고 손가락을 (조심스럽게) 들어 올리면 어떤 페이지로도 스크롤되지 않고 거기서 멈 춥니 다.
— Christian A. Strømmen

@ ChristianA.Strømmen Weird, 그것은 내 앱과 잘 작동합니다.
— 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)
}

몇 가지 참고 사항 :

  • 결함이 없습니다
  • 페이징을 거짓으로 설정하십시오 ! (그렇지 않으면 작동하지 않습니다)
  • 자신 만의 flickvelocity를 쉽게 설정할 수 있습니다.
  • 이것을 시도한 후에도 여전히 작동하지 않는 경우 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
당신은 생명의 구세주입니다! PAGING을 FALSE로 설정하는 것이 중요합니다 !!! ... 내 인생 2 시간 이미 작동하는 함수를 수정처럼 분실
— denis631

트윗 담아 가기 추가 했어야했는데이를 반영하도록 게시물을 편집하겠습니다! 그것은 일했다 다행 :)
— JoniVR

jesssus, 난 내가 페이징을 사용하지 않도록 설정하는 방법에 대한이 댓글을 볼 때까지이 과정의 광산이 true로 설정 ... 작동하지 않는 이유를 궁금 해서요
— 우와 캄

@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 2013

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 2014 년

올바르게 이해했는지 확실하지 않지만 항목을 중앙에서 시작하고 중앙에 맞추려면 contentInset을 변경해야합니다. 나는 이것을 사용한다 : gist.github.com/pionl/432fc8059dee3b540e38
— Pion

셀의 X 위치를 뷰 중앙에 맞추려면 속도 섹션에서 + (layoutAttributes.frame.size.width / 2)를 제거하면됩니다.
— PION

1
@Jay Hi, 사용자 지정 흐름 대리자를 만들고이 코드를 추가하십시오. 펜촉 또는 코드에서 사용자 정의 레이아웃을 설정하는 것을 잊지 마십시오.
— Pion 16.04.25

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
스위프트 3이의 업데이트 버전 : gist.github.com/mstubna/beed10327e00310d05f12bf4747266a4
— mstubna

1
Dang it @mstubna, 나는 계속해서 위의 내용을 복사하고 swift 3으로 업데이트하고 업데이트 된 요점을 만들기 시작하고 여기로 돌아와서 메모 / 제목을 수집했습니다. 감사! 너무 아쉽다.
— VaporwareWolf

16

다음과 같은 솔루션을 찾는 사람을 위해 ...

  • 사용자가 짧은 빠른 스크롤을 수행 할 때 글리치하지 않습니다 (예 : 양수 및 음수 스크롤 속도 고려).
  • 걸립니다 collectionView.contentInset (아이폰 X에와 safeArea)은 고려
  • 스크롤 지점에서 볼 수있는 셀만 고려 (성능을 위해)
  • 잘 명명 된 변수와 주석을 사용합니다.
  • 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
반환 cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left -이 라인에 문제가 sectionInset.left - collectionView.safeAreaInsets.left candidateOffsets
— Utku Dalmaz

1
@Dalmaz 알려 주셔서 감사합니다. 지금 문제를 해결했습니다.
— Oliver Pearmain

1
네, 방금 복사하여 붙여 넣으면 시간이 절약됩니다.
— Wei

7

수평 스크롤 컬렉션 뷰에 대한 제 스위프트 솔루션입니다. 간단하고 달콤하며 깜박임을 방지합니다.

  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 의 코드

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 버전 (현재 Swift 5)에서 작동하는지 확인하기 위해 답변을 사용했습니다. @ André Abreu 을 몇 가지 정보를 추가합니다.

UICollectionViewFlowLayout을 서브 클래 싱 할 때 "override func awakeFromNib () {}"가 작동하지 않습니다 (이유를 모릅니다). 대신 "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.