targetContentOffsetForProposedContentOffset: withScrollingVelocity sin subclasificar UICollectionViewFlowLayout

99

Tengo un collectionView muy simple en mi aplicación (solo una fila de imágenes cuadradas en miniatura).

Me gustaría interceptar el desplazamiento para que el desplazamiento siempre deje una imagen completa en el lado izquierdo. Por el momento, se desplaza a cualquier lugar y dejará imágenes cortadas.

De todos modos, sé que necesito usar la función

- (CGPoint)targetContentOffsetForProposedContentOffset:withScrollingVelocity

para hacer esto, pero solo estoy usando un estándar UICollectionViewFlowLayout. No lo estoy subclasificando.

¿Hay alguna forma de interceptar esto sin subclasificar UICollectionViewFlowLayout?

Gracias

Fogmeister
fuente

Respuestas:

113

Bien, la respuesta es no, no hay manera de hacer esto sin subclasificar UICollectionViewFlowLayout.

Sin embargo, subclasificarlo es increíblemente fácil para cualquiera que lea esto en el futuro.

Primero configuré la llamada de subclase MyCollectionViewFlowLayouty luego en el generador de interfaz cambié el diseño de la vista de colección a Personalizado y seleccioné mi subclase de diseño de flujo.

Debido a que lo está haciendo de esta manera, no puede especificar tamaños de elementos, etc.en IB, así que en MyCollectionViewFlowLayout.m tengo esto ...

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

Esto configura todos los tamaños para mí y la dirección de desplazamiento.

Luego ...

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

Esto asegura que el desplazamiento finalice con un margen de 5,0 en el borde izquierdo.

Eso es todo lo que necesitaba hacer. No necesitaba establecer el diseño de flujo en código en absoluto.

Fogmeister
fuente
1
Es realmente poderoso cuando se usa correctamente. ¿Ha visto las sesiones de Vista de colección de WWDC 2012? Realmente vale la pena verlos. Algunas cosas increíbles.
Fogmeister
2
targetContentOffsetForProposedContentOffset:withVelocityno me llaman cuando me desplazo. ¿Que esta pasando?
fatuhoku
4
@TomSawyer estableció la tasa de declaración de UICollectionView en UIScrollViewDecelerationRateFast.
Clay Ellis
3
@fatuhoku asegúrese de que la propiedad paginEnabled de su collectionView esté configurada en falso
chrs
4
Holy Moly, tuve que desplazarme hacia abajo como un millón de millas para ver esta respuesta. :)
AnBisw
67

La solución de Dan es defectuosa. No maneja bien el movimiento del usuario. Los casos en los que el usuario hace un movimiento rápido y el desplazamiento no se mueve tanto, tienen problemas de animación.

Mi implementación alternativa propuesta tiene la misma paginación que la propuesta anteriormente, pero maneja al usuario desplazarse entre páginas.

 #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;
 }
DarthMike
fuente
¡Gracias! Funcionó como por arte de magia. Un poco difícil de entender pero llegar.
Rajiev Timal
Tengo este error: ¿No se puede asignar a 'x' en 'propuestoContentOffset'? ¿Utilizas Swift? ¿Cómo puedo asignar un valor x?
TomSawyer
@TomSawyer Los parámetros están 'permitidos' por defecto. Intente declarar la función como esta en Swift (usando var antes de param): anule func targetContentOffsetForProposedContentOffset (var propuestoContentOffset: CGPoint) -> CGPoint
DarthMike
1
No puede usar CGPointMake en rápido. Yo personalmente utilicé esto: "var targetContentOffset: CGPoint if pannedLessThanAPage && flicked {targetContentOffset = CGPoint (x: nextPage * pageWidth (), y: propuestoContentOffset.y);} else {targetContentOffset = CGPoint (x: round (rawPageValue) * pageWidth ( ), y :jectedContentOffset.y);} return propositContentOffset "
Gráfico
1
Debe ser la respuesta seleccionada.
khunshan
26

Versión rápida de la respuesta aceptada.

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

Válido para Swift 5 .

André Abreu
fuente
Esta versión funciona muy bien, y también funciona bien para el eje Y si intercambias el código.
Chris
Mayormente funciona muy bien aquí. Pero si dejo de desplazarme y levanto el dedo (con cuidado), no se desplazará a ninguna página y se detendrá allí.
Christian A. Strømmen
@ ChristianA.Strømmen Extraño, funciona bien con mi aplicación.
André Abreu
@ AndréAbreu ¿dónde coloco esta función?
FlowUI. SimpleUITesting.com
2
@Jay Necesita subclase UICollectionViewLayout o cualquier clase que ya la haya subclasificado (por ejemplo, UICollectionViewFlowLayout).
André Abreu
24

Aquí está mi implementación en Swift 5 para la paginación vertical basada en celdas:

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

Algunas notas:

  • No falla
  • FIJAR PAGINA EN FALSO ! (de lo contrario, esto no funcionará)
  • Le permite configurar su propia velocidad de flick fácilmente.
  • Si algo aún no funciona después de intentar esto, verifique si itemSizerealmente coincide con el tamaño del artículo, ya que a menudo es un problema, especialmente cuando se usacollectionView(_:layout:sizeForItemAt:) , use una variable personalizada con itemSize en su lugar.
  • Esto funciona mejor cuando lo configura self.collectionView.decelerationRate = UIScrollView.DecelerationRate.fast.

Aquí hay una versión horizontal (no la he probado a fondo, así que perdone cualquier error):

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

Este código se basa en el código que utilizo en mi proyecto personal, puede consultarlo aquí descargándolo y ejecutando el destino de ejemplo.

JoniVR
fuente
4
¡Eres el salvador de la vida! Es importante tener en cuenta que CONFIGURAR LA PAGINA EN FALSA !!! Perdí como 2 horas de mi vida arreglando tu función, que ya funciona ...
denis631
@ denis631 ¡Lo siento mucho! Debería haber agregado eso, ¡editaré la publicación para reflejar esto! Me alegro de que haya funcionado :)
JoniVR
Jesús, me preguntaba por qué esto no funciona hasta que vi este comentario sobre la desactivación de la paginación ... por supuesto, el mío se estableció en verdadero
Kam Wo
@JoniVR Me muestra que este método de error no anula ningún método de su superclase
Muju
22

Si bien esta respuesta ha sido de gran ayuda para mí, hay un parpadeo notable cuando se desliza rápido en una pequeña distancia. Es mucho más fácil reproducirlo en el dispositivo.

Descubrí que esto siempre sucede cuando collectionView.contentOffset.x - proposedContentOffset.xy velocity.xtengo diferentes canciones.

Mi solución fue asegurarme de que proposedContentOffsetsea ​​más que contentOffset.xsi la velocidad es positiva y menos si es negativa. Está en C # pero debería ser bastante simple de traducir a Objective C:

public override PointF TargetContentOffset (PointF proposedContentOffset, PointF scrollingVelocity)
{
    /* Determine closest edge */

    float offSetAdjustment = float.MaxValue;
    float horizontalCenter = (float) (proposedContentOffset.X + (this.CollectionView.Bounds.Size.Width / 2.0));

    RectangleF targetRect = new RectangleF (proposedContentOffset.X, 0.0f, this.CollectionView.Bounds.Size.Width, this.CollectionView.Bounds.Size.Height);
    var array = base.LayoutAttributesForElementsInRect (targetRect);

    foreach (var layoutAttributes in array) {
        float itemHorizontalCenter = layoutAttributes.Center.X;
        if (Math.Abs (itemHorizontalCenter - horizontalCenter) < Math.Abs (offSetAdjustment)) {
            offSetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
    }

    float nextOffset = proposedContentOffset.X + offSetAdjustment;

    /*
     * ... unless we end up having positive speed
     * while moving left or negative speed while moving right.
     * This will cause flicker so we resort to finding next page
     * in the direction of velocity and use it.
     */

    do {
        proposedContentOffset.X = nextOffset;

        float deltaX = proposedContentOffset.X - CollectionView.ContentOffset.X;
        float velX = scrollingVelocity.X;

        // If their signs are same, or if either is zero, go ahead
        if (Math.Sign (deltaX) * Math.Sign (velX) != -1)
            break;

        // Otherwise, look for the closest page in the right direction
        nextOffset += Math.Sign (scrollingVelocity.X) * SnapStep;
    } while (IsValidOffset (nextOffset));

    return proposedContentOffset;
}

bool IsValidOffset (float offset)
{
    return (offset >= MinContentOffset && offset <= MaxContentOffset);
}

Este código está usando MinContentOffset, MaxContentOffsety SnapStepdebería ser trivial para definir. En mi caso resultaron ser

float MinContentOffset {
    get { return -CollectionView.ContentInset.Left; }
}

float MaxContentOffset {
    get { return MinContentOffset + CollectionView.ContentSize.Width - ItemSize.Width; }
}

float SnapStep {
    get { return ItemSize.Width + MinimumLineSpacing; }
}
Dan Abramov
fuente
7
Esto funciona muy bien. Lo convertí a Objective-C para aquellos interesados: gist.github.com/rkeniger/7687301
Rob Keniger
21

Después de largas pruebas, encontré una solución para ajustar al centro con un ancho de celda personalizado (cada celda tiene un ancho diferencial) que corrige el parpadeo. Siéntete libre de mejorar el guión.

- (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;
}
Pion
fuente
10
La mejor solución de todas, ¡gracias! Además, para los futuros lectores, debe desactivar la paginación para que esto funcione.
sridvijay
1
Si uno quisiera alinearlo desde la izquierda, en lugar de alinear la celda a la derecha en el centro, ¿cómo haríamos para cambiarlo?
CyberMew
No estoy seguro de si lo entiendo correctamente, pero si desea iniciar los elementos en el centro y alinearlos con el centro, debe cambiar el contentInset. Yo uso esto: gist.github.com/pionl/432fc8059dee3b540e38
Pion
Para alinear en la posición X de la celda a la mitad de la vista, simplemente elimine + (layoutAttributes.frame.size.width / 2) en la sección de velocidad.
Pion
1
@Jay Hola, solo crea un delegado de Flow personalizado y agrégale este código. No olvide configurar el diseño personalizado en plumilla o código.
Pion
18

consulte esta respuesta de Dan Abramov aquí está la versión 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;
}

o resumen aquí https://gist.github.com/katopz/8b04c783387f0c345cd9

katopz
fuente
4
Versión actualizada de esto para Swift 3: gist.github.com/mstubna/beed10327e00310d05f12bf4747266a4
mstubna
1
Maldita sea @mstubna, seguí adelante y copié lo anterior, lo actualicé a swift 3, comencé a hacer una esencia actualizada y volví aquí para recopilar notas / título, momento en el que noté que ya había hecho una esencia rápida de 3. ¡Gracias! Lástima que me lo perdí.
VaporwareWolf
16

Para cualquiera que busque una solución que ...

  • NO SE PROBLEMA cuando el usuario realiza un desplazamiento rápido corto (es decir, considera velocidades de desplazamiento positivas y negativas)
  • toma el collectionView.contentInset en consideración (y safeArea en iPhone X)
  • solo considera las celdas visibles en el punto de desplazamiento (para rendimiento)
  • usa variables y comentarios bien nombrados
  • es Swift 4

entonces por favor mira abajo ...

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
        }
    }
}
Oliver Pearmain
fuente
1
¡Gracias! simplemente copiado y pegado, funcionando perfectamente .. mucho mejor como se esperaba.
Steven B.
1
Una y única solución que realmente funciona. ¡Buen trabajo! ¡Gracias!
LinusGeffarth
1
return cellLayoutAttributes.frame.origin.x - collectionView.contentInset.left - collectionView.safeAreaInsets.left candidatoOffsets - sectionInset.left hay un problema en esta línea
Utku Dalmaz
1
@Dalmaz gracias por avisarme. He solucionado el problema ahora.
Oliver Pearmain
1
Sí, recién copiado y pegado, me ahorras tiempo.
Wei
7

Aquí está mi solución Swift en una vista de colección de desplazamiento horizontal. Es simple, dulce y evita cualquier parpadeo.

  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
  }
Scott Kaiser
fuente
que es itemSize??
Konstantinos Natsios
Es el tamaño de las celdas de recolección. Estas funciones se utilizan cuando se subclasifican UICollectionViewFlowLayout.
Scott Kaiser
1
Me gusta esta solución, pero tengo un par de comentarios. pageWidth()debe usar minimumLineSpacingya que se desplaza horizontalmente. Y en mi caso, tengo una contentInsetvista de colección para que la primera y la última celda se puedan centrar, así que uso let xOffset = pageWidth() * index - collectionView.contentInset.left.
blwinters
6

un pequeño problema que encontré al usar targetContentOffsetForProposedContentOffset es un problema con la última celda que no se ajusta de acuerdo con el nuevo punto que devolví.
Descubrí que el CGPoint que devolví tenía un valor Y mayor que el permitido, así que usé el siguiente código al final de mi implementación targetContentOffsetForProposedContentOffset:

// if the calculated y is bigger then the maximum possible y we adjust accordingly
CGFloat contentHeight = self.collectionViewContentSize.height;
CGFloat collectionViewHeight = self.collectionView.bounds.size.height;
CGFloat maxY = contentHeight - collectionViewHeight;
if (newY > maxY)
{
    newY = maxY;
}

return CGPointMake(0, newY);

para que quede más claro, esta es mi implementación de diseño completo que solo imita el comportamiento de paginación vertical:

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

con suerte esto le ahorrará a alguien algo de tiempo y dolor de cabeza

keisar
fuente
1
El mismo problema, parece que la vista de colección ignorará los valores no válidos en lugar de redondearlos a sus límites.
Mike M
6

Prefiero permitir que el usuario hojee varias páginas. Así que aquí está mi versión de targetContentOffsetForProposedContentOffset(que se basa en la respuesta de DarthMike) para el diseño vertical .

- (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;
}
Anton Gaenko
fuente
4

La respuesta de Fogmeisters funcionó para mí a menos que me desplazara hasta el final de la fila. Mis celdas no encajan perfectamente en la pantalla, por lo que se desplazaría hasta el final y saltaría hacia atrás con un tirón para que la última celda siempre se superpusiera al borde derecho de la pantalla.

Para evitar esto, agregue la siguiente línea de código al comienzo del método targetcontentoffset

if(proposedContentOffset.x>self.collectionViewContentSize.width-320-self.sectionInset.right)
    return proposedContentOffset;
Ajaxharg
fuente
Supongo que 320 es el ancho de vista de tu colección :)
Au Ris
Me encanta mirar hacia atrás en el código antiguo. Supongo que ese número mágico fue ese.
Ajaxharg
2

@ André Abreu 's Code

Versión 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)
    }
}
Cruz
fuente
¡Gracias por eso! El mejor comportamiento esperado ¡Gracias es Ayuda mucho!
G Clovs
2

Rápido 4

La solución más sencilla para la vista de colección con celdas de un tamaño (desplazamiento 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
}
lobstah
fuente
2

Una solución más corta (asumiendo que está almacenando en caché sus atributos de diseño):

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

Para poner esto en contexto:

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)
    }
}
Niels
fuente
1

Para asegurarme de que funciona en la versión Swift (swift 5 ahora), usé la respuesta de @ André Abreu, agrego más información:

Cuando se subclasifica UICollectionViewFlowLayout, el "override func awakeFromNib () {}" no funciona (no sé por qué). En su lugar, usé "anular init () {super.init ()}"

Este es mi código puesto en la clase 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

}

Después de subclasificar, asegúrese de poner esto en ViewDidLoad ():

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

Para aquellos que buscan una solución en 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)
    }
}
Husein Kareem
fuente