iphone - ¿Cómo se determina el espaciado entre celdas en UICollectionView flowLayout?
objective-c ios (11)
Actualización: versión rápida de esta respuesta: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift
Tomando la delantera de @ matt modifiqué su código para asegurar que los artículos SIEMPRE queden alineados. Descubrí que si un artículo terminaba en una línea por sí mismo, se centraría en el diseño del flujo. Hice los siguientes cambios para abordar este problema.
Esta situación solo ocurrirá si tiene celdas que varían en ancho, lo que podría generar un diseño como el siguiente. La última línea siempre a la izquierda se alinea debido al comportamiento de UICollectionViewFlowLayout
, el problema radica en los elementos que están solos en cualquier línea, excepto la última.
Con el código de @ matt que estaba viendo.
En ese ejemplo, vemos que las células se centran si terminan en la línea por sí mismas. El siguiente código asegura que su vista de colección se vería así.
#import "CWDLeftAlignedCollectionViewFlowLayout.h"
const NSInteger kMaxCellSpacing = 9;
@implementation CWDLeftAlignedCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) {
if (nil == attributes.representedElementKind) {
NSIndexPath* indexPath = attributes.indexPath;
attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame;
}
}
return attributesToReturn;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes* currentItemAttributes =
[super layoutAttributesForItemAtIndexPath:indexPath];
UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset];
if (indexPath.item == 0) { // first item of section
CGRect frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
CGFloat previousFrameRightPoint = previousFrame.origin.x + previousFrame.size.width + kMaxCellSpacing;
CGRect currentFrame = currentItemAttributes.frame;
CGRect strecthedCurrentFrame = CGRectMake(0,
currentFrame.origin.y,
self.collectionView.frame.size.width,
currentFrame.size.height);
if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
// the approach here is to take the current frame, left align it to the edge of the view
// then stretch it the width of the collection view, if it intersects with the previous frame then that means it
// is on the same line, otherwise it is on it''s own new line
CGRect frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
CGRect frame = currentItemAttributes.frame;
frame.origin.x = previousFrameRightPoint;
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
@end
Tengo un UICollectionView con un diseño de flujo y cada celda es un cuadrado. ¿Cómo determino el espacio entre cada celda en cada fila? Parece que no puedo encontrar la configuración adecuada para esto. Veo que hay un mínimo de espaciado de atributos en el archivo de punta para una vista de colección, pero configuré esto en 0 y las células ni siquiera se pegan.
¿Alguna otra idea?
Aquí está para NSCollectionViewFlowLayout
class LeftAlignedCollectionViewFlowLayout: NSCollectionViewFlowLayout {
var maximumCellSpacing = CGFloat(2.0)
override func layoutAttributesForElementsInRect(rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect)
for attributes in attributesToReturn ?? [] {
if attributes.representedElementKind == nil {
attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath!)!.frame
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> NSCollectionViewLayoutAttributes? {
let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
let sectionInset = (self.collectionView?.collectionViewLayout as! NSCollectionViewFlowLayout).sectionInset
if indexPath.item == 0 {
let f = curAttributes!.frame
curAttributes!.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height)
return curAttributes
}
let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath)!.frame
let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing
let curFrame = curAttributes!.frame
let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height)
if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
curAttributes!.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
} else {
curAttributes!.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
}
return curAttributes
}
}
El "problema" con UICollectionViewFlowLayout
es que aplica una alineación justificada a las celdas: la primera celda en una fila se alinea a la izquierda, la última celda en una fila se alinea a la derecha y todas las otras celdas intermedias se distribuyen uniformemente con un igual espaciado que es mayor que el minimumInteritemSpacing
.
Ya hay muchas respuestas excelentes para esta publicación que resuelven este problema subclasificando UICollectionViewFlowLayout
. Como resultado, obtiene un diseño que alinea las celdas restantes . Otra solución válida para distribuir las celdas con un espaciado constante es alinear las celdas a la derecha .
AlignedCollectionViewFlowLayout
También creé una subclase UICollectionViewFlowLayout
que sigue una idea similar sugerida por matt y Chris Wagner que puede alinear las celdas
⬅︎ izquierda :
o ➡︎ derecha :
Simplemente puede descargarlo desde aquí, agregar el archivo de diseño a su proyecto y establecer AlignedCollectionViewFlowLayout
como la clase de diseño de su vista de colección:
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout
Cómo funciona (para celdas alineadas a la izquierda):
+---------+----------------------------------------------------------------+---------+
| | | |
| | +------------+ | |
| | | | | |
| section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
| inset | |intersection| | | line rect | inset |
| |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| |
| (left) | | | current item | (right) |
| | +------------+ | |
| | previous item | |
+---------+----------------------------------------------------------------+---------+
El concepto aquí es verificar si la celda actual con índice iy la celda anterior con el índice i-1 ocupan la misma línea.
- Si no lo hacen, la celda con índice i es la celda más a la izquierda de la línea.
→ Mueva la celda al borde izquierdo de la vista de colección (sin cambiar su posición vertical). - Si lo hacen, la celda con índice i no es la celda más a la izquierda en la línea.
→ Obtenga el marco de la celda anterior (con el índice i-1 ) y mueva la celda actual al lado.
Para celdas alineadas a la derecha ...
... haces lo mismo al revés, es decir, que marques la siguiente celda con el índice i + 1 en su lugar.
Hay algunas cosas a considerar:
Intente cambiar el espaciado mínimo en IB, pero deje el cursor en ese campo. Observe que Xcode no marca inmediatamente el documento como cambiado. Sin embargo, cuando hace clic en un campo diferente, Xcode se da cuenta de que el documento ha cambiado y lo marca en el navegador de archivos. Por lo tanto, asegúrese de tabular o hacer clic en un campo diferente después de hacer un cambio.
Guarde su archivo storyboard / xib después de hacer un cambio, y asegúrese de reconstruir la aplicación. No es difícil perderse ese paso, y luego te estás rascando la cabeza preguntándote por qué los cambios no parecían tener ningún efecto.
UICollectionViewFlowLayout
tiene una propiedadminimumInteritemSpacing
, que es lo que está configurando en IB. Pero el delegado de la colección también puede tener un método para determinar el espaciado entre elementos . Ese método prevalece sobre la propiedad del diseño, por lo que si lo implementa en su delegado no se usará la propiedad de su diseño.Recuerde que el espaciado allí es un espacio mínimo . El diseño usará ese número (ya sea que provenga de la propiedad o del método delegado) como el espacio más pequeño permitido, pero puede usar un espacio más grande si tiene espacio sobrante en la línea. Entonces, si, por ejemplo, establece el espaciado mínimo en 0, aún puede ver algunos píxeles entre los elementos. Si desea tener más control sobre cómo se espacian los elementos, probablemente debería usar un diseño diferente (posiblemente uno de su propia creación).
La versión rápida de la solución Chris.
class PazLeftAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout {
var maxCellSpacing = 14.0
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
if var attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? Array<UICollectionViewLayoutAttributes> {
for attributes in attributesToReturn {
if attributes.representedElementKind == nil {
let indexPath = attributes.indexPath
attributes.frame = self.layoutAttributesForItemAtIndexPath(indexPath).frame;
}
}
return attributesToReturn;
}
return super.layoutAttributesForElementsInRect(rect)
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
if let collectionViewFlowLayout = self.collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
let sectionInset = collectionViewFlowLayout.sectionInset
if (indexPath.item == 0) { // first item of section
var frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
let previousIndexPath = NSIndexPath(forItem:indexPath.item-1, inSection:indexPath.section)
let previousFrame = self.layoutAttributesForItemAtIndexPath(previousIndexPath).frame;
let previousFrameRightPoint = Double(previousFrame.origin.x) + Double(previousFrame.size.width) + self.maxCellSpacing
let currentFrame = currentItemAttributes.frame
var width : CGFloat = 0.0
if let collectionViewWidth = self.collectionView?.frame.size.width {
width = collectionViewWidth
}
let strecthedCurrentFrame = CGRectMake(0,
currentFrame.origin.y,
width,
currentFrame.size.height);
if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
// the approach here is to take the current frame, left align it to the edge of the view
// then stretch it the width of the collection view, if it intersects with the previous frame then that means it
// is on the same line, otherwise it is on it''s own new line
var frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
var frame = currentItemAttributes.frame;
frame.origin.x = CGFloat(previousFrameRightPoint)
currentItemAttributes.frame = frame;
}
return currentItemAttributes;
}
}
Para usarlo haz lo siguiente:
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.collectionViewLayout = self.layout
}
var layout : PazLeftAlignedCollectionViewFlowLayout {
var layout = PazLeftAlignedCollectionViewFlowLayout()
layout.itemSize = CGSizeMake(220.0, 230.0)
layout.minimumLineSpacing = 12.0
return layout
}
Para obtener un espaciado entre sitios máximo, la subclase UICollectionViewFlowLayout y sobrescribe layoutAttributesForElementsInRect:
y layoutAttributesForItemAtIndexPath:
Por ejemplo, un problema común es este: las filas de una vista de colección están justificadas a la derecha e izquierda, a excepción de la última línea que está justificada a la izquierda. Digamos que queremos que todas las líneas estén justificadas a la izquierda, de modo que el espacio entre ellas sea, digamos, 10 puntos. Aquí hay una manera fácil (en su subclase UICollectionViewFlowLayout):
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray* arr = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes* atts in arr) {
if (nil == atts.representedElementKind) {
NSIndexPath* ip = atts.indexPath;
atts.frame = [self layoutAttributesForItemAtIndexPath:ip].frame;
}
}
return arr;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes* atts =
[super layoutAttributesForItemAtIndexPath:indexPath];
if (indexPath.item == 0) // degenerate case 1, first item of section
return atts;
NSIndexPath* ipPrev =
[NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
CGRect fPrev = [self layoutAttributesForItemAtIndexPath:ipPrev].frame;
CGFloat rightPrev = fPrev.origin.x + fPrev.size.width + 10;
if (atts.frame.origin.x <= rightPrev) // degenerate case 2, first item of line
return atts;
CGRect f = atts.frame;
f.origin.x = rightPrev;
atts.frame = f;
return atts;
}
La razón por la que esto es tan fácil es que no realizamos realmente el trabajo pesado del diseño; estamos aprovechando el trabajo de diseño que UICollectionViewFlowLayout ya ha hecho por nosotros. Ya ha decidido cuántos elementos van en cada línea; solo estamos leyendo esas líneas y empujando los elementos juntos, si entiendes lo que quiero decir.
Solución Clean Swift, de una historia de evolución:
- hubo respuesta mate
- había elementos solitarios Chris Wagner arreglar
- hubo mokagio sectionInset y minimumInteritemSpacing improvement
- había https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift
- ahora aquí hay una versión simplificada y limpia de la mía:
open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 }
}
open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes,
collectionView != nil else {
// should never happen
return nil
}
// if the current frame, once stretched to the full row intersects the previous frame then they are on the same row
if indexPath.item != 0,
let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame,
currentItemAttributes.frame.intersects(CGRect(x: -.infinity, y: previousFrame.origin.y, width: .infinity, height: previousFrame.size.height)) {
// the next item on a line
currentItemAttributes.frame.origin.x = previousFrame.origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
} else {
// the first item on a line
currentItemAttributes.frame.origin.x = evaluatedSectionInsetForSection(at: indexPath.section).left
}
return currentItemAttributes
}
func evaluatedMinimumInteritemSpacingForSection(at section: NSInteger) -> CGFloat {
return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section) ?? minimumInteritemSpacing
}
func evaluatedSectionInsetForSection(at index: NSInteger) -> UIEdgeInsets {
return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: index) ?? sectionInset
}
}
Uso: el espacio entre elementos está determinado por collectionView (_:layout:minimumInteritemSpacingForSectionAt:)
.
Lo puse en github, https://github.com/Coeur/UICollectionViewLeftAlignedLayout , donde en realidad agregué una función de compatibilidad con ambas direcciones de desplazamiento (horizontal y vertical).
Un poco de matemáticas hace el truco más fácilmente. El código escrito por Chris Wagner es horrible porque llama a los atributos de diseño de cada elemento anterior. Así que cuanto más se desplaza, más lento es ...
Simplemente use módulo como este (estoy usando mi valor mínimoInterEmSpacing como un valor máximo también):
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes* currentItemAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
NSInteger numberOfItemsPerLine = floor([self collectionViewContentSize].width / [self itemSize].width);
if (indexPath.item % numberOfItemsPerLine != 0)
{
NSInteger cellIndexInLine = (indexPath.item % numberOfItemsPerLine);
CGRect itemFrame = [currentItemAttributes frame];
itemFrame.origin.x = ([self itemSize].width * cellIndexInLine) + ([self minimumInteritemSpacing] * cellIndexInLine);
currentItemAttributes.frame = itemFrame;
}
return currentItemAttributes;
}
Una forma fácil de justificar a la izquierda es modificar layoutAttributesForElementsInRect: en su subclase de UICollectionViewFlowLayout:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *allLayoutAttributes = [super layoutAttributesForElementsInRect:rect];
CGRect prevFrame = CGRectMake(-FLT_MAX, -FLT_MAX, 0, 0);
for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes)
{
//fix blur
CGRect theFrame = CGRectIntegral(layoutAttributes.frame);
//left justify
if(prevFrame.origin.x > -FLT_MAX &&
prevFrame.origin.y >= theFrame.origin.y &&
prevFrame.origin.y <= theFrame.origin.y) //workaround for float == warning
{
theFrame.origin.x = prevFrame.origin.x +
prevFrame.size.width +
EXACT_SPACE_BETWEEN_ITEMS;
}
prevFrame = theFrame;
layoutAttributes.frame = theFrame;
}
return allLayoutAttributes;
}
Una versión más rápida y limpia para las personas interesadas, basada en la respuesta de Chris Wagner:
class AlignLeftFlowLayout: UICollectionViewFlowLayout {
var maximumCellSpacing = CGFloat(9.0)
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]
for attributes in attributesToReturn ?? [] {
if attributes.representedElementKind == nil {
attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath).frame
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
let sectionInset = (self.collectionView?.collectionViewLayout as UICollectionViewFlowLayout).sectionInset
if indexPath.item == 0 {
let f = curAttributes.frame
curAttributes.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height)
return curAttributes
}
let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath).frame
let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing
let curFrame = curAttributes.frame
let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height)
if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
curAttributes.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
} else {
curAttributes.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
}
return curAttributes
}
}
una base de versión rápida en mokagio: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift
class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect)
if let attributesToReturn = attributesToReturn {
for attributes in attributesToReturn {
if attributes.representedElementKind == nil {
let indexpath = attributes.indexPath
if let attr = layoutAttributesForItemAtIndexPath(indexpath) {
attributes.frame = attr.frame
}
}
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
if let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath){
let sectionInset = self.evaluatedSectionInsetForItemAtIndex(indexPath.section)
let isFirstItemInSection = indexPath.item == 0;
let layoutWidth = CGRectGetWidth(self.collectionView!.frame) - sectionInset.left - sectionInset.right;
if (isFirstItemInSection) {
currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset)
return currentItemAttributes
}
let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
let previousFrame = layoutAttributesForItemAtIndexPath(previousIndexPath)?.frame ?? CGRectZero
let previousFrameRightPoint = previousFrame.origin.x + previousFrame.width
let currentFrame = currentItemAttributes.frame;
let strecthedCurrentFrame = CGRectMake(sectionInset.left,
currentFrame.origin.y,
layoutWidth,
currentFrame.size.height)
// if the current frame, once left aligned to the left and stretched to the full collection view
// widht intersects the previous frame then they are on the same line
let isFirstItemInRow = !CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)
if (isFirstItemInRow) {
// make sure the first item on a line is left aligned
currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset)
return currentItemAttributes
}
var frame = currentItemAttributes.frame;
frame.origin.x = previousFrameRightPoint + evaluatedMinimumInteritemSpacingForSectionAtIndex(indexPath.section)
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
return nil
}
func evaluatedMinimumInteritemSpacingForSectionAtIndex(sectionIndex:Int) -> CGFloat {
if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout {
if delegate.respondsToSelector("collectionView:layout:minimumInteritemSpacingForSectionAtIndex:") {
return delegate.collectionView!(self.collectionView!, layout: self, minimumInteritemSpacingForSectionAtIndex: sectionIndex)
}
}
return self.minimumInteritemSpacing
}
func evaluatedSectionInsetForItemAtIndex(index: Int) ->UIEdgeInsets {
if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout {
if delegate.respondsToSelector("collectionView:layout:insetForSectionAtIndex:") {
return delegate.collectionView!(self.collectionView!, layout: self, insetForSectionAtIndex: index)
}
}
return self.sectionInset
}
}