programing

제안된 내용 간격띄우기에 대한 대상 내용 간격띄우기:UICollectionViewFlowLayout을 하위 분류하지 않고 스크롤 속도 사용

muds 2023. 5. 31. 18:35
반응형

제안된 내용 간격띄우기에 대한 대상 내용 간격띄우기:UICollectionViewFlowLayout을 하위 분류하지 않고 스크롤 속도 사용

앱에 매우 단순한 모음 뷰(사각형 섬네일 이미지 한 줄만 있음)가 있습니다.

오프셋이 항상 왼쪽에 전체 이미지를 남기도록 스크롤을 가로채고 싶습니다.지금은 아무 곳에나 스크롤되고 잘라진 이미지가 남습니다.

어쨌든, 나는 그 기능을 사용해야 한다는 것을 압니다.

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

는 그냥 하만난 지그사있고뿐을다입니용하기을준저▁a▁using▁standard▁to다▁i뿐니m▁just'입▁do있을.UICollectionViewFlowLayout하위 분류가 아닙니다.

을 하위 분류하지 이 있나요?UICollectionViewFlowLayout?

감사해요.

좋아요. 대답은 아니오입니다. UICollectionViewFlowLayout을 하위 분류하지 않고는 이 작업을 수행할 수 없습니다.

하지만, 그것을 하위 분류하는 것은 미래에 이것을 읽는 사람이라면 누구나 믿을 수 없을 정도로 쉽습니다.

합니다.MyCollectionViewFlowLayout그런 다음 인터페이스 작성기에서 수집 보기 레이아웃을 사용자 정의로 변경하고 흐름 레이아웃 하위 클래스를 선택했습니다.

이런 식으로 작업하기 때문에 항목 크기 등을 지정할 수 없습니다.IBso에서 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의 여백으로 스크롤이 종료됩니다.

그게 제가 할 일의 전부였습니다.코드로 흐름 레이아웃을 설정할 필요가 전혀 없었습니다.

댄의 해결책은 결함이 있습니다.사용자 플리킹을 잘 처리하지 못합니다.사용자가 빠르게 깜박이고 스크롤을 많이 움직이지 않는 경우 애니메이션 결함이 있습니다.

제가 제안한 대체 구현은 이전에 제안한 것과 동일한 페이지를 가지고 있지만 페이지 간 사용자 이동을 처리합니다.

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

승인된 답변의 빠른 버전입니다.

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에 유효합니다.

수직 셀 기반 페이징을 위한 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:)대신 크기 항목과 함께 사용자 지정 변수를 사용합니다.
  • 은 설할때가잘작니다합동장정다▁you니▁best를 설정할 때 잘 작동합니다.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)
}

이 코드는 제 개인 프로젝트에서 사용하는 코드를 기반으로 합니다. 여기서 다운로드하여 예제 대상을 실행하면 확인할 수 있습니다.

해결책을 찾는 사람들을 위해...

  • 사용자가 짧은 빠른 스크롤을 수행할 때(즉, 양 및 음의 스크롤 속도를 고려함) 글리치가 발생하지 않습니다.
  • collectionView.contentInsetX의 안전한 ) 사항 (및 iPhone X 안전영역고) 려중.
  • 스크롤 시점에 표시되는 셀만 고려합니다(성능의 경우
  • 잘 명명된 변수와 주석을 사용
  • 스위프트 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
        }
    }
}

답변은 저에게 큰 도움이 되었지만, 작은 거리에서 빠르게 스와이프할 때 눈에 띄는 깜박임이 있습니다.그것을 기기에 재현하는 것이 훨씬 쉽습니다.

저는 이런 일이 항상 일어난다는 것을 발견했습니다.collectionView.contentOffset.x - proposedContentOffset.x그리고.velocity.x노래가 다릅니다.

제 해결책은 다음과 같은 것을 보장하는 것이었습니다.proposedContentOffset 입니다.contentOffset.x속도가 양수이면 속도가 음수이면 속도가 감소합니다.C#으로 되어 있지만 목표 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; }
}

오랜 테스트 끝에 깜박임을 고정하는 맞춤형 셀 폭(각 셀의 너비가 다름)으로 중앙에 스냅할 수 있는 솔루션을 찾았습니다.스크립트를 자유롭게 개선하십시오.

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

아브라모프의 답변을 참조하십시오. 여기 스위프트 버전이 있습니다.

override func targetContentOffset(
    forProposedContentOffset proposedContentOffset: CGPoint,
    withScrollingVelocity velocity: CGPoint
) -> CGPoint {
    var _proposedContentOffset = CGPoint(
        x: proposedContentOffset.x, y: proposedContentOffset.y
    )
    var offSetAdjustment: CGFloat = CGFloat.greatestFiniteMagnitude
    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.layoutAttributesForElements(in: targetRect)!
        as [UICollectionViewLayoutAttributes]
    for layoutAttributes: UICollectionViewLayoutAttributes in array {
        if layoutAttributes.representedElementCategory == UICollectionView.ElementCategory.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(offset: 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
}

orgist 여기 https://gist.github.com/katopz/8b04c783387f0c345cd9

수평으로 스크롤되는 컬렉션 뷰에 대한 나의 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
  }

반환한 새입니다.targetContentOffsetForProposedContentOffset의 문제입니다.
반환한 CGPoint의 Y 값이 더 컸지만 허용됨을 알게 되어 대상 ContentOffsetForProposedContentOffset 구현의 마지막에 다음 코드를 사용했습니다.

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

바라건대 이것이 누군가의 시간과 두통을 덜어줄 것입니다.

사용자가 여러 페이지를 휙휙 넘길 수 있도록 허용하는 것을 선호합니다.여기 제 버전이 있습니다.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;
}

행 끝까지 스크롤하지 않는 한 포그 마이스터의 대답은 효과가 있었습니다.제 셀이 화면에 잘 맞지 않아서 끝까지 스크롤했다가 갑자기 뒤로 점프해서 마지막 셀이 항상 화면의 오른쪽 가장자리와 겹치도록 했습니다.

이를 방지하려면 대상 컨텐츠 오프셋 메서드의 시작 부분에 다음 코드 행을 추가합니다.

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

@앙드레 아브레우의 법전

스위프트 3 버전

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

스위프트 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
}

더 짧은 솔루션(레이아웃 속성을 캐싱한다고 가정):

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

스위프트 버전(현재 스위프트 5)에서 작동하는지 확인하기 위해 @Andre Aveu의 답변을 사용하여 다음과 같은 정보를 추가했습니다.

UICollectViewFlowLayout을 하위 분류할 때 "funcakeFromNib(){} 재정의함"이 작동하지 않습니다(이유를 알 수 없음).대신 "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

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

CollectionView에 대한 것이 아니라 더 잘 작동합니다.이것은 내가 본 것 중 최고의 해결책입니다.
사용하기만 하면 됩니다..linear유형.

https://github.com/nicklockwood/iCarousel

저자에게 신의 가호가 있기를!:)

올리버의 대답은 저에게 완벽하게 먹혔지만, 불필요한 반복이 있었습니다.따라서 이를 기반으로 훨씬 더 간단한 구현을 만들었습니다.

override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
                                         withScrollingVelocity velocity: CGPoint) -> CGPoint {
    guard let collectionView = collectionView else {
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
    }
    
    /// the actual page width without any insets or margins
    let pageContentWidth = self.itemSize.width
    /// total page width
    let pageWidth = pageContentWidth + self.minimumLineSpacing
    /// current page estimate based on the proposed contentoffset
    let proposedPage = round(proposedContentOffset.x / pageWidth)
    
    /// getting the proposed page content offset (the start position of the page we are supposed to take rest at)
    let proposedXContentOffsetBasedOnProposedPage = proposedPage * pageWidth
    
    /// minimum allowed content offset
    let minContentOffset: CGFloat = 0
    /// maximum allowed content offset
    let maxContentOffset = collectionViewContentSize.width-pageContentWidth
    
    let bestCandidateOffset: CGFloat
    /// if the velocity is positive ( the user is scrolling to the right)
    if velocity.x > 0 {
        /// the scroll final position should always advance to the RIGHT; hence if the `proposedXContentOffsetBasedOnProposedPage` is LESS than the original proposed offset this means that the estimated page was estimated wrong and it will go back. However, because the velocity is to right this means that we need to go to the NEXT page directly hence (proposedPage+1)
        let nearestCandidateOffsetToRight = proposedXContentOffsetBasedOnProposedPage < proposedContentOffset.x ?
        (proposedPage+1)*pageWidth :
        proposedXContentOffsetBasedOnProposedPage
        
        /// making sure not to go out of contentSize bounds
        bestCandidateOffset = min(maxContentOffset, nearestCandidateOffsetToRight)
    } else if velocity.x < 0 { /// if the velocity is negative ( the user is scrolling to the left)
        /// the scroll final position should always advance to the LEFT; hence if the `proposedXContentOffsetBasedOnProposedPage` is GREATER than the original proposed offset this means that the estimated page was estimated wrong and it will move forward. However, because the velocity is to left this means that we need to go to the PREVIOUS page directly hence (proposedPage-1)
        let nearestCandidateOffsetToLeft = proposedXContentOffsetBasedOnProposedPage > proposedContentOffset.x ?
        (proposedPage-1)*pageWidth :
        proposedXContentOffsetBasedOnProposedPage
        
        /// making sure not to go out of contentSize bounds
        bestCandidateOffset = max(minContentOffset, nearestCandidateOffsetToLeft)
    } else { /// if the velocity is 0
        /// move directly to the `proposedXContentOffsetBasedOnProposedPage`
        bestCandidateOffset = proposedXContentOffsetBasedOnProposedPage
    }
    
    return CGPoint(x: bestCandidateOffset, y: proposedContentOffset.y)
}

이 솔루션과 여기에 언급된 다른 솔루션을 사용해 본 경우 너무 빨리 스크롤하면 여러 페이지를 건너뛸 수 있으며, isPagingEnabled가 true로 설정된 경우에는 그렇지 않습니다.

이 구현은 UICollectionView의 최종 정지 위치가 항상 페이지와 일치하도록 보장합니다.한 번에 한 페이지로 스크롤하는 제한은 고려하지 않습니다.

속도에 관계없이 스크롤을 한 번에 한 페이지로 제한하려면 UIScrollView의 scrollViewWillBeginDraging 메서드를 구현하고 사용자가 스크롤을 시작한 페이지를 저장할 수 있습니다.그런 다음 ScrollViewWillEndDraging 메서드에서 시작 페이지를 기준으로 최종 정지 위치를 결정할 수 있습니다.

/// used to save the page the user started the scroll from
var startScrollPage: Int?
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    /// the actual page width without any insets or margins
    let pageContentWidth = collectionView.collectionViewLayout.itemSize.width
    /// total page width
    let pageWidth = pageContentWidth + collectionView.collectionViewLayout.minimumLineSpacing
    /// current page estimate based on the proposed contentoffset
    let currentPage = round(scrollView.contentOffset.x / pageWidth)
    
    startScrollPage = Int(currentPage)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView,
                               withVelocity velocity: CGPoint,
                               targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    guard let startScrollPage = startScrollPage else { return }
    /// the actual page width without any insets or margins
    let pageContentWidth = collectionView.collectionViewLayout.itemSize.width
    /// total page width
    let pageWidth = pageContentWidth + collectionView.collectionViewLayout.minimumLineSpacing
    /// current page estimate based on the proposed contentoffset
    let proposedPage = round(targetContentOffset.pointee.x / pageWidth)
    
    /// getting the proposed page content offset (the start position of the page we are supposed to take rest at)
    let proposedXContentOffsetBasedOnProposedPage = proposedPage * pageWidth
    /// minimum allowed content offset
    let minContentOffset: CGFloat = 0
    /// maximum allowed content offset
    let maxContentOffset = scrollView.contentSize.width-pageContentWidth
    
    let bestCandidateOffset: CGFloat
    /// if the velocity is positive ( the user is scrolling to the right)
    if velocity.x > 0 {
        /// get the next page offset by adding 1 to the starting page
        let nearestCandidateOffsetToRight = CGFloat(startScrollPage+1)*pageWidth
        
        /// making sure not to go out of contentSize bounds
        bestCandidateOffset = min(maxContentOffset, nearestCandidateOffsetToRight)
    } else if velocity.x < 0 { /// if the velocity is negative ( the user is scrolling to the left)
        /// get the prev page offset by subtracting 1 from the starting page
        let nearestCandidateOffsetToLeft = CGFloat(startScrollPage-1)*pageWidth
        
        /// making sure not to go out of contentSize bounds
        bestCandidateOffset = max(minContentOffset, nearestCandidateOffsetToLeft)
    } else { /// if the velocity is 0
        /// move directly to the `proposedXContentOffsetBasedOnProposedPage`
        bestCandidateOffset = proposedXContentOffsetBasedOnProposedPage
    }
    
    targetContentOffset.pointee.x = bestCandidateOffset
    self.startScrollPage = nil
}

다음은 셀별 페이징 데모입니다. 하나 이상의 셀을 건너뛰지 않고 빠르게 스크롤할 때 https://github.com/ApesTalk/ATPagingByCell

언급URL : https://stackoverflow.com/questions/13492037/targetcontentoffsetforproposedcontentoffsetwithscrollingvelocity-without-subcla

반응형