targetContentOffsetForProposedContentOffset: withScrollingVelocity tanpa subclassing UICollectionViewFlowLayout


99

Saya punya collectionView yang sangat sederhana di aplikasi saya (hanya satu baris gambar thumbnail persegi).

Saya ingin menghentikan pengguliran sehingga offset selalu menyisakan gambar penuh di sisi kiri. Saat ini menggulir ke mana saja dan akan meninggalkan gambar yang terpotong.

Bagaimanapun, saya tahu saya perlu menggunakan fungsinya

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

untuk melakukan ini tetapi saya hanya menggunakan standar UICollectionViewFlowLayout. Saya tidak subclassing itu.

Apakah ada cara untuk mencegat ini tanpa subclass UICollectionViewFlowLayout?

Terima kasih

Jawaban:


113

Oke, jawabannya tidak, tidak ada cara untuk melakukan ini tanpa subclass UICollectionViewFlowLayout.

Namun, membuat subclass sangat mudah bagi siapa saja yang membaca ini di masa mendatang.

Pertama saya mengatur panggilan subkelas MyCollectionViewFlowLayoutdan kemudian di pembuat antarmuka saya mengubah tata letak tampilan koleksi menjadi Kustom dan memilih subkelas tata letak aliran saya.

Karena Anda melakukannya dengan cara ini, Anda tidak dapat menentukan ukuran item, dll ... di IB jadi di MyCollectionViewFlowLayout.m saya punya ini ...

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

Ini mengatur semua ukuran untuk saya dan arah gulir.

Kemudian ...

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

Ini memastikan bahwa pengguliran berakhir dengan margin 5.0 di tepi kiri.

Hanya itu yang perlu saya lakukan. Saya tidak perlu mengatur tata letak aliran dalam kode sama sekali.


1
Ini sangat kuat saat digunakan dengan benar. Sudahkah Anda menonton sesi Tampilan Koleksi dari WWDC 2012? Mereka sangat layak untuk ditonton. Beberapa hal luar biasa.
Fogmeister

2
targetContentOffsetForProposedContentOffset:withVelocitytidak dipanggil untuk saya saat saya menggulir. Apa yang sedang terjadi?
fatuhoku

4
@TomSawyer menyetel laju deklarasi UICollectionView ke UIScrollViewDecelerationRateFast.
Clay Ellis

3
@fatuhoku pastikan bahwa properti paginEnabled collectionView Anda disetel ke false
chrs

4
Holy Moly, saya harus menggulung ke bawah seperti jutaan mil untuk melihat jawaban ini. :)
AnBisw

67

Solusi Dan cacat. Ini tidak menangani jentikan pengguna dengan baik. Kasus-kasus ketika pengguna menjentikkan cepat dan gulir tidak banyak bergerak, memiliki gangguan animasi.

Implementasi alternatif yang saya usulkan memiliki paginasi yang sama seperti yang diusulkan sebelumnya, tetapi menangani pengguna yang beralih antar halaman.

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

Terima kasih! Ini bekerja seperti pesona. Agak sulit untuk dipahami tetapi untuk mencapainya.
Rajiev Timal

Saya mengalami kesalahan ini: Tidak dapat menetapkan ke 'x' di 'usulanContentOffset'? Menggunakan swift? bagaimana saya dapat menetapkan nilai x?
TomSawyer

Parameter @TomSawyer adalah 'biarkan' secara default. Cobalah untuk mendeklarasikan fungsi seperti ini di Swift (menggunakan var sebelum param): override func targetContentOffsetForProposedContentOffset (var usulanContentOffset: CGPoint) -> CGPoint
DarthMike

1
Anda tidak dapat menggunakan CGPointMake dengan cepat. Saya pribadi menggunakan ini: "var targetContentOffset: CGPoint if pannedLessThanAPage && flicked {targetContentOffset = CGPoint (x: nextPage * pageWidth (), y: usulanContentOffset.y);} lain {targetContentOffset = CGPoint (x: round (rawPageValue) * pageWidth ( ), y: usulanContentOffset.y);} kembalikan usulanContentOffset "
Plot

1
Itu harus menjadi jawaban yang dipilih.
khunshan

26

Versi cepat dari jawaban yang diterima.

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

Berlaku untuk Swift 5 .


Versi ini berfungsi dengan baik, dan berfungsi dengan baik untuk sumbu Y juga jika Anda menukar kodenya.
Chris

Sebagian besar berfungsi dengan baik di sini. Tetapi jika saya berhenti menggulir, dan mengangkat jari saya (hati-hati), itu tidak akan menggulir ke halaman mana pun dan hanya berhenti di situ.
Christian A. Strømmen

@ ChristianA.Strømmen Aneh, Ini berfungsi dengan baik dengan aplikasi saya.
André Abreu

@ AndréAbreu di mana saya menempatkan fungsi ini?
FlowUI. SimpleUITesting.com

2
@Jay Anda perlu membuat subkelas UICollectionViewLayout atau kelas apa pun yang sudah membuat subkelasnya (mis. UICollectionViewFlowLayout).
André Abreu

24

Inilah implementasi saya di Swift 5 untuk paging berbasis sel vertikal :

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

Beberapa catatan:

  • Tidak ada kesalahan
  • ATUR HALAMAN KE SALAH ! (jika tidak ini tidak akan berhasil)
  • Memungkinkan Anda mengatur flickvelocity Anda sendiri dengan mudah.
  • Jika ada sesuatu yang masih tidak berfungsi setelah mencoba ini, periksa apakah Anda itemSizebenar - benar cocok dengan ukuran item karena itu sering menjadi masalah, terutama saat menggunakan collectionView(_:layout:sizeForItemAt:), gunakan variabel khusus dengan itemSize sebagai gantinya.
  • Ini bekerja paling baik saat Anda menyetel self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Ini adalah versi horizontal (belum mengujinya secara menyeluruh jadi mohon maafkan jika ada kesalahan):

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

Kode ini didasarkan pada kode yang saya gunakan dalam proyek pribadi saya, Anda dapat memeriksanya di sini dengan mengunduhnya dan menjalankan target Contoh.


4
Anda adalah penyelamat hidup! Penting untuk diperhatikan untuk SET PAGING TO FALSE !!! Hilang seperti 2 jam dalam hidup saya memperbaiki fungsi Anda, yang sudah berfungsi ...
denis631

@ denis631 Maafkan saya! Saya harus menambahkan itu, saya akan mengedit posting untuk mencerminkan ini! Senang itu berhasil :)
JoniVR

jesssus, saya bertanya-tanya mengapa ini tidak berfungsi sampai saya melihat komentar ini tentang menonaktifkan paging ... tentu saja milik saya disetel ke true
Kam Wo

@JoniVR Ini menunjukkan kepada saya Metode kesalahan ini tidak menimpa metode apa pun dari superclass-nya
Muju

22

Meskipun jawaban ini sangat membantu saya, ada kedipan yang terlihat saat Anda menggesek cepat pada jarak yang kecil. Jauh lebih mudah untuk mereproduksinya di perangkat.

Saya menemukan bahwa ini selalu terjadi ketika collectionView.contentOffset.x - proposedContentOffset.xdan velocity.xmemiliki nyanyian yang berbeda.

Solusi saya adalah memastikan bahwa proposedContentOffsetlebih daricontentOffset.x jika kecepatan positif, dan lebih sedikit jika negatif. Ada di C # tetapi harus cukup sederhana untuk diterjemahkan ke Tujuan 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);
}

Kode ini sedang digunakan MinContentOffset, MaxContentOffsetdan SnapStepyang seharusnya sepele untuk Anda tentukan. Dalam kasus saya, mereka ternyata

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
Ini bekerja dengan sangat baik. Saya mengonversinya menjadi Objective-C bagi mereka yang tertarik: gist.github.com/rkeniger/7687301
Rob Keniger

21

Setelah pengujian yang lama saya menemukan solusi untuk diposisikan ke tengah dengan lebar sel khusus (setiap sel memiliki lebar yang berbeda) yang memperbaiki kedipan. Silakan perbaiki skripnya.

- (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
Solusi terbaik dari semuanya, terima kasih! Juga untuk setiap pembaca di masa mendatang, Anda harus mematikan paging agar ini berfungsi.
sridvijay

1
Jika seseorang ingin meratakannya dari kiri, alih-alih sel sejajar tepat di tengah, bagaimana kita akan mengubahnya?
CyberMew

Tidak yakin apakah saya mengerti dengan benar, tetapi jika Anda ingin memulai item di tengah, dan menyelaraskannya ke tengah, Anda perlu mengubah contentInset. Saya menggunakan ini: gist.github.com/pionl/432fc8059dee3b540e38
Pion

Untuk menyelaraskan posisi X sel ke tengah tampilan, hapus saja + (layoutAttributes.frame.size.width / 2) di bagian kecepatan.
Pion

1
@Jay Hai, buat saja delegasi Flow kustom dan tambahkan kode ini ke dalamnya. Jangan lupa untuk mengatur tata letak khusus di ujung pena atau kode.
Pion

18

lihat jawaban ini dari Dan Abramov berikut ini versi 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;
}

atau intinya di sini https://gist.github.com/katopz/8b04c783387f0c345cd9


4
Versi yang diperbarui untuk Swift 3: gist.github.com/mstubna/beed10327e00310d05f12bf4747266a4
mstubna

1
Dang itu @mstubna, saya melanjutkan dan menyalin yang di atas, memperbaruinya menjadi swift 3, mulai membuat inti yang diperbarui, dan kembali ke sini untuk mengumpulkan catatan / judul di mana saya perhatikan bahwa Anda telah membuat 3 inti yang cepat. Terima kasih! Sayang sekali aku melewatkannya.
VaporwareWolf

16

Untuk siapapun yang mencari solusi yang ...

  • JANGAN GLITCH saat pengguna melakukan scroll cepat pendek (misalnya, mempertimbangkan kecepatan scroll positif dan negatif)
  • mempertimbangkan collectionView.contentInset(dan safeArea di iPhone X)
  • hanya menganggap sel-sel ini terlihat pada titik pengguliran (untuk kinerja)
  • menggunakan variabel dan komentar yang dinamai dengan baik
  • adalah Swift 4

lalu silahkan lihat dibawah ...

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
Terima kasih! baru saja disalin & ditempel, bekerja dengan sempurna .. jauh lebih baik seperti yang diharapkan.
Steven B.

1
Satu-satunya solusi yang benar - benar berfungsi. Pekerjaan yang baik! Terima kasih!
LinusGeffarth

1
kembalikan cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left kandidatOffsets - sectionInset.left ada masalah di baris ini
Utku Dalmaz

1
@ Dalmaz terima kasih telah memberi tahu saya. Saya telah memperbaiki masalahnya sekarang.
Oliver Pearmain

1
Ya, baru disalin & ditempel, Anda menghemat waktu saya.
Wei

7

Inilah solusi Swift saya pada tampilan koleksi yang bergulir secara horizontal. Sederhana, manis, dan menghindari kedipan.

  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
  }

apa itemSize??
Konstantinos Natsios

Ini adalah ukuran sel koleksi. Fungsi ini digunakan saat membuat subclass UICollectionViewFlowLayout.
Scott Kaiser


1
Saya suka solusi ini, tetapi saya punya beberapa komentar. pageWidth()harus digunakan minimumLineSpacingkarena menggulung secara horizontal. Dan dalam kasus saya, saya memiliki tampilan contentInsetuntuk koleksi sehingga sel pertama dan terakhir bisa dipusatkan, jadi saya gunakan let xOffset = pageWidth() * index - collectionView.contentInset.left.
blwinters

6

masalah kecil yang saya temui saat menggunakan targetContentOffsetForProposedContentOffset adalah masalah dengan sel terakhir yang tidak menyesuaikan sesuai dengan poin baru yang saya kembalikan.
Saya menemukan bahwa CGPoint yang saya kembalikan memiliki nilai Y yang lebih besar daripada yang diizinkan, jadi saya menggunakan kode berikut di akhir implementasi targetContentOffsetForProposedContentOffset saya:

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

hanya untuk membuatnya lebih jelas ini adalah implementasi tata letak lengkap saya yang hanya meniru perilaku paging vertikal:

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

mudah-mudahan ini akan menghemat waktu dan sakit kepala seseorang


1
Masalah yang sama, sepertinya tampilan koleksi akan mengabaikan nilai yang tidak valid alih-alih membulatkannya ke batasnya.
Mike M

6

Saya lebih suka mengizinkan pengguna membolak-balik beberapa halaman. Jadi inilah versi saya targetContentOffsetForProposedContentOffset(yang berdasarkan jawaban DarthMike) untuk tata letak vertikal .

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

Jawaban Fogmeisters berhasil untuk saya kecuali saya menggulir ke akhir baris. Sel saya tidak pas dengan rapi di layar sehingga akan bergulir ke ujung dan melompat kembali dengan sentakan sehingga sel terakhir selalu tumpang tindih dengan tepi kanan layar.

Untuk mencegahnya, tambahkan baris kode berikut di awal metode targetcontentoffset

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

Saya kira 320 adalah lebar tampilan koleksi Anda :)
Au Ris

Harus senang melihat kembali kode lama. Saya kira angka ajaib itu.
Ajaxharg

2

Kode @ André Abu

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

Terima kasih untuk itu! Perilaku terbaik yang diharapkan. Terima kasih. Sangat membantu!
G Clovs

2

Cepat 4

Solusi termudah untuk tampilan koleksi dengan sel satu ukuran (gulir horizontal):

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

Solusi yang lebih singkat (dengan asumsi Anda sedang menyimpan atribut tata letak Anda):

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

Untuk menempatkan ini dalam konteks:

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

Untuk memastikannya berfungsi dalam versi Swift (swift 5 sekarang), saya menggunakan jawabannya dari @ André Abreu, saya menambahkan beberapa informasi lagi:

Saat membuat subkelas UICollectionViewFlowLayout, "override func awakeFromNib () {}" tidak bekerja (tidak tahu mengapa). Sebagai gantinya, saya menggunakan "override init () {super.init ()}"

Ini kode saya yang diletakkan di kelas 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

}

Setelah membuat subclass, pastikan untuk meletakkan ini di ViewDidLoad ():

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

0

Bagi mereka yang mencari solusi di 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)
    }
}

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.