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.