targetContentOffsetForProposedContentOffset: withScrollingVelocity, alt sınıflandırma olmadan UICollectionViewFlowLayout


100

Uygulamamda çok basit bir collectionView var (sadece tek bir kare küçük resim satırı).

Kaydırma işlemini durdurmak istiyorum, böylece ofset her zaman sol tarafta tam bir görüntü bırakır. Şu anda her yere kayar ve kesik görüntüler bırakır.

Her neyse, işlevi kullanmam gerektiğini biliyorum

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

bunu yapmak için ama sadece bir standart kullanıyorum UICollectionViewFlowLayout. Alt sınıfa ayırmıyorum.

Bunu alt sınıflara ayırmadan yakalamanın bir yolu var mı UICollectionViewFlowLayout?

Teşekkürler

Yanıtlar:


114

Tamam, cevap hayır, UICollectionViewFlowLayout alt sınıflamadan bunu yapmanın bir yolu yok.

Ancak, alt sınıflara ayırmak, gelecekte bunu okuyan herkes için inanılmaz derecede kolaydır.

Önce alt sınıf çağrısını kurdum MyCollectionViewFlowLayoutve ardından arayüz oluşturucuda koleksiyon görünümü düzenini Özel olarak değiştirdim ve akış düzeni alt sınıfımı seçtim.

Bunu bu şekilde yaptığınız için, IB'de öğe boyutlarını vb. Belirtemezsiniz, bu yüzden MyCollectionViewFlowLayout.m'de bu var ...

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

Bu benim için tüm boyutları ve kaydırma yönünü ayarlar.

Sonra ...

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

Bu, kaydırmanın sol kenarda 5,0 kenar boşluğu ile bitmesini sağlar.

Tüm yapmam gereken buydu. Akış düzenini kodda ayarlamama hiç gerek yoktu.


1
Doğru kullanıldığında gerçekten güçlüdür. WWDC 2012'deki Koleksiyon Görünümü oturumlarını izlediniz mi? Gerçekten izlemeye değer. Bazı inanılmaz şeyler.
Fogmeister

2
targetContentOffsetForProposedContentOffset:withVelocitykaydırdığımda benim için çağrılmıyor. Neler oluyor?
fatuhoku

4
@TomSawyer, UICollectionView bildirim oranını UIScrollViewDecelerationRateFast olarak ayarladı.
Kil Ellis

4
@fatuhoku collectionView’ınızın paginEnabled özelliğinin false olarak ayarlandığından emin olun
chrs

4
Kutsal Moly, bu cevabı görmek için bir milyon mil kadar aşağı gitmem gerekiyordu. :)
AnBisw

67

Dan'in çözümü kusurlu. Kullanıcının titremesini iyi idare etmez. Kullanıcının hızlıca kaydırdığı ve kaydırmanın çok fazla hareket etmediği durumlarda, animasyon sorunları var.

Önerilen alternatif uygulamam, daha önce önerilenle aynı sayfalandırmaya sahip, ancak sayfalar arasında kullanıcı geçişini gerçekleştiriyor.

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

Teşekkür ederim! Bu harika çalıştı. Anlaması biraz zor ama oraya ulaşmak.
Rajiev ve optimal

Şu hatayı alıyorum: 'önerilenContentOffset' içindeki 'x' atanamıyor mu? Swift mi kullanıyorsunuz? x değerini nasıl atayabilirim?
TomSawyer

@TomSawyer Params varsayılan olarak 'izin verilir'. Swift'de işlevi bu şekilde bildirmeyi deneyin (
param'dan

1
CGPointMake'i hızlı bir şekilde kullanamazsınız. Bunu kişisel olarak kullandım: "var targetContentOffset: pannedLessThanAPage && flicked ise CGPoint {targetContentOffset = CGPoint (x: nextPage * pageWidth (), y: recommendedContentOffset.y);} else {targetContentOffset = CGPoint (x: round (rawPageValue) * pageWPoint ), y: proposedContentOffset.y)} "proposedContentOffset geri
Plot

1
Seçilen cevap olmalıdır.
khunshan

26

Kabul edilen cevabın hızlı versiyonu.

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 için geçerlidir .


Bu sürüm harika çalışıyor ve kodu değiştirirseniz Y ekseni için de iyi çalışıyor.
Chris

Çoğunlukla burada harika çalışıyor. Ancak kaydırmayı bırakırsam ve parmağımı (dikkatlice) kaldırırsam, herhangi bir sayfaya kaymaz ve orada durur.
Christian A. Strømmen

@ ChristianA.Strømmen Garip, Uygulamamla gayet iyi çalışıyor.
André Abreu

@ AndréAbreu bu işlevi nereye yerleştirmeliyim?
FlowUI. SimpleUITesting.com

2
@Jay UICollectionViewLayout veya halihazırda alt sınıflara sahip herhangi bir sınıfı alt sınıflamanız gerekir (örn. UICollectionViewFlowLayout).
André Abreu

24

Dikey hücre tabanlı sayfalama için Swift 5'teki uygulamam :

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

Bazı notlar:

  • Aksaklık yapmaz
  • SAYFALARI YANLIŞ OLARAK AYARLAYIN ! (aksi takdirde bu işe yaramaz)
  • Kendi hareket hızınızı kolayca ayarlamanıza olanak sağlar .
  • Bunu denedikten sonra hala bir şey çalışmıyorsa itemSize, öğenin boyutuyla gerçekten eşleşip eşleşmediğini kontrol edin, çünkü bu genellikle bir sorundur, özellikle kullanırken collectionView(_:layout:sizeForItemAt:), bunun yerine itemSize ile özel bir değişken kullanın.
  • Bu, ayarladığınızda en iyi şekilde çalışır self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

İşte yatay bir versiyon (tam olarak test etmedik, bu yüzden lütfen hataları affedin):

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

Bu kod, kişisel projemde kullandığım koda dayanmaktadır, buradan indirip Örnek hedefi çalıştırarak kontrol edebilirsiniz .


4
Sen hayat kurtarıcısın! SAYFALARI YANLIŞ OLARAK AYARLAMAK İÇİN ÖNEMLİ !!! Hayatımın 2 saatini, zaten işe yarayan işlevini düzeltmek için kaybettim ...
denis631

@ denis631 Çok üzgünüm! Bunu eklemeliydim, yazıyı bunu yansıtacak şekilde düzenleyeceğim! Çalıştığına sevindim :)
JoniVR

jesssus, ben sayfalama devre dışı bırakma hakkında şu yorumu görene kadar bu tabii ki maden true kuruldu ... neden çalışmadığını merak
Wo Kam

@JoniVR Bana bu hatayı gösteriyor Yöntem, üst sınıfından herhangi bir yöntemi geçersiz kılmaz
Muju

22

İken bu cevabı bana çok yardımcı olmuştur küçük bir mesafeye hızlı kaydırma yaptığınızda, farkedilir bir titreşim var. Cihazda çoğaltmak çok daha kolay.

Ben bu her zaman gerçekleşir bulundu collectionView.contentOffset.x - proposedContentOffset.xve velocity.xfarklı söylüyor var.

Benim çözümüm, bunun hızın pozitif proposedContentOffsetolduğundan daha fazla contentOffset.x, negatifse daha az olmasını sağlamaktı . C # içindedir, ancak Amaç C'ye çevrilmesi oldukça basit olmalıdır:

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

Bu kod kullanıyor MinContentOffset, MaxContentOffsetve SnapSteptanımlamak için hangi Önemsiz olmalıdır. Benim durumumda ortaya çıktılar

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
Bu gerçekten iyi çalışıyor. İlgilenenler için bunu Objective-C'ye çevirdim
Rob Keniger

21

Uzun testlerden sonra, titremeyi düzelten özel hücre genişliğiyle (her hücre farklı genişliğe sahiptir) merkeze oturtmak için bir çözüm buldum. Senaryoyu geliştirmekten çekinmeyin.

- (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
Hepsinden en iyi çözüm, teşekkürler! Ayrıca gelecekteki okuyucular için, bunun çalışması için sayfalamayı kapatmanız gerekir.
sridvijay

1
Ortada sağa hizalanmış hücre yerine soldan hizalamak isteseydi, onu nasıl değiştirirdik?
CyberMew

Doğru anladığımdan emin değilim, ancak öğeleri ortalamak ve merkeze hizalamak istiyorsanız, contentInset'i değiştirmeniz gerekir. Bunu kullanıyorum: gist.github.com/pionl/432fc8059dee3b540e38
Pion

Hücrenin X konumunda görünümün ortasına hizalamak için, hız bölümünde + (layoutAttributes.frame.size.width / 2) öğesini kaldırmanız yeterlidir.
Pion

1
@Jay Merhaba, özel bir Flow temsilcisi oluşturun ve bu kodu ona ekleyin. Uç veya kodda özel düzeni ayarlamayı unutmayın.
Pion

18

bakın Dan Abramov tarafından bu cevap burada Swift versiyonu

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

veya buradan özetle https://gist.github.com/katopz/8b04c783387f0c345cd9


4
Swift 3 için güncellenmiş versiyonu: gist.github.com/mstubna/beed10327e00310d05f12bf4747266a4
mstubna

1
Dang it @mstubna, devam ettim ve yukarıdakileri kopyaladım, hızlı 3 olarak güncelledim, güncellenmiş bir özet oluşturmaya başladım ve notları / başlığı toplamak için buraya geri döndüm ve bu noktada hızlı bir 3 öz yaptığınızı fark ettim. Teşekkürler! Ne yazık ki kaçırdım.
VaporwareWolf

18

Bir çözüm arayanlar için ...

  • Kullanıcı kısa bir hızlı kaydırma yaptığında (yani, pozitif ve negatif kaydırma hızlarını dikkate alırsa) PARLATMAZ
  • sürer collectionView.contentInset(iPhone X ve safeArea) dikkate
  • yalnızca kaydırma noktasında görünen hücreleri dikkate alır (performans için)
  • iyi adlandırılmış değişkenler ve yorumlar kullanır
  • Swift 4

o zaman lütfen aşağıya bakın ...

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
Teşekkürler! sadece kopyalandı ve yapıştırıldı, mükemmel çalışıyor .. beklendiği gibi daha iyi.
Steven B.

1
Gerçekten işe yarayan tek çözüm . İyi iş! Teşekkürler!
LinusGeffarth

1
return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left candidateOffsets - sectionInset.left bu satırda bir sorun var
Utku Dalmaz

1
@Dalmaz beni bilgilendirdiğiniz için teşekkürler. Şimdi sorunu çözdüm.
Oliver Pearmain

1
Evet, kopyalayıp yapıştırdım, zamanımı kurtarıyorsun.
Wei

7

İşte benim Swift çözümüm, yatay olarak kaydırılan bir koleksiyon görünümünde. Basit, tatlı ve titremeyi önlüyor.

  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
  }

nedir itemSize??
Konstantinos Natsios

Toplama hücrelerinin boyutu. Bu işlev, UICollectionViewFlowLayout alt sınıflarını oluştururken kullanılır.
Scott Kaiser


1
Bu çözümü beğendim ama birkaç yorumum var. yatay kaydırdığı için pageWidth()kullanılmalıdır minimumLineSpacing. Ve benim durumumda, contentInsetkoleksiyon görünümü için bir tane var, böylece ilk ve son hücre ortalanabilir, bu yüzden kullanıyorum let xOffset = pageWidth() * index - collectionView.contentInset.left.
blwinters

6

targetContentOffsetForProposedContentOffset'i kullanırken karşılaştığım küçük bir sorun, döndürdüğüm yeni noktaya göre son hücrenin ayarlanmaması ile ilgili bir sorundur.
Döndürdüğüm CGPoint'in daha büyük bir Y değerine sahip olduğunu ve ardından izin verildiğini öğrendim, bu nedenle targetContentOffsetForProposedContentOffset uygulamamın sonunda aşağıdaki kodu kullandım:

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

daha açık hale getirmek için bu, dikey sayfalama davranışını taklit eden tam düzen uygulamam:

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

umarım bu birisini biraz zaman ve baş ağrısından kurtarır


1
Aynı sorun, koleksiyon görünümü geçersiz değerleri sınırlarına yuvarlamak yerine yok sayacak gibi görünüyor.
Mike M

6

Kullanıcının birkaç sayfada gezinmesine izin vermeyi tercih ederim. İşte dikey yerleşim targetContentOffsetForProposedContentOffsetiçin (DarthMike cevabına dayanan) versiyonum .

- (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 cevabı, satırın sonuna kadar kaydırmadığım sürece benim için çalıştı. Hücrelerim ekrana düzgün bir şekilde sığmıyor, bu yüzden son hücre her zaman ekranın sağ kenarıyla üst üste gelecek şekilde sona kayacak ve bir sarsıntıyla geri atlayacaktı.

Bunu önlemek için targetcontentoffset yönteminin başlangıcına aşağıdaki kod satırını ekleyin

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

Sanırım 320 koleksiyon görünüm genişliğinizdir :)
Au Ris

Eski kurallara bakmayı sevmelisin. Sanırım o sihirli sayı buydu.
Ajaxharg

2

@ André Abreu'nun Kodu

Swift3 sürümü

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

Bunun için teşekkürler! Beklenen en iyi davranış Teşekkürler Çok yardımcı oluyor!
G Clovs

2

Swift 4

Tek boyutlu hücrelerle koleksiyon görünümü için en kolay çözüm (yatay kaydırma):

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

Daha kısa bir çözüm (düzen özniteliklerinizi önbelleğe aldığınızı varsayarak):

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

Bunu bağlama koymak için:

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 sürümünde çalıştığından emin olmak için (şimdi hızlı 5), @ André Abreu'nun cevabını kullandım, biraz daha bilgi ekledim:

UICollectionViewFlowLayout alt sınıfını oluştururken, "geçersiz kılma func awakeFromNib () {}" çalışmıyor (nedenini bilmiyorum). Bunun yerine "override init () {super.init ()}" kullandım

Bu benim SubclassFlowLayout: UICollectionViewFlowLayout {} sınıfına yerleştirilen kodum:

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

}

Alt sınıflandırma sonrasında, bunu ViewDidLoad () içine koyduğunuzdan emin olun:

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

0

Swift'de çözüm arayanlar için:

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

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.