Изменить порядок ячеек UICollectionView

Рассмотрим UICollectionView с макетом потока и горизонтальным направлением. По умолчанию ячейки располагаются сверху вниз, слева направо. Как это:

1 4 7 10 13 16
2 5 8 11 14 17
3 6 9 12 15 18

В моем случае представление коллекции разбито на страницы и разработано так, чтобы на каждой странице помещалось определенное количество ячеек. Таким образом, более естественный порядок будет:

1 2 3   10 11 12
4 5 6 - 13 14 15
7 8 9   16 17 18

Что было бы самым простым для достижения этой цели, кроме реализации моей собственной пользовательской компоновки? В частности, я не хочу терять какие-либо функции, которые предоставляются бесплатно с UICollectionViewFlowLayout (например, вставить / удалить анимацию).

Или вообще как реализовать функцию переупорядочения f(n) на макете потока? Например, то же самое можно применить к порядку справа налево.

Мой подход пока

Мой первый подход был к подклассу UICollectionViewFlowLayout и переопределить layoutAttributesForItemAtIndexPath::

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSIndexPath *reorderedIndexPath = [self reorderedIndexPathOfIndexPath:indexPath];
    UICollectionViewLayoutAttributes *layout = [super layoutAttributesForItemAtIndexPath:reorderedIndexPath];
    layout.indexPath = indexPath;
    return layout;
}

куда reorderedIndexPathOfIndexPath: является f(n), По телефону superМне не нужно рассчитывать расположение каждого элемента вручную.

Дополнительно пришлось переопределить layoutAttributesForElementsInRect:, который метод использует макет для выбора элементов для отображения.

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSMutableArray *result = [NSMutableArray array];
    NSInteger sectionCount = 1;
    if ([self.collectionView.dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)])
    {
        sectionCount = [self.collectionView.dataSource numberOfSectionsInCollectionView:self.collectionView];
    }
    for (int s = 0; s < sectionCount; s++)
    {
        NSInteger itemCount = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:s];
        for (int i = 0; i < itemCount; i++)
        {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:s];
            UICollectionViewLayoutAttributes *layout = [self layoutAttributesForItemAtIndexPath:indexPath];
            if (CGRectIntersectsRect(rect, layout.frame))
            {
                [result addObject:layout];
            }
        }
    }
    return result;
}

Здесь я просто пробую каждый элемент, и если он находится в заданном rectЯ возвращаю это.

Если этот подход является подходом, у меня есть следующие более конкретные вопросы:

  • Есть ли способ, которым я могу упростить layoutAttributesForElementsInRect: переопределить или сделать его более эффективным?
  • Я что-то пропустил? По крайней мере, замена ячеек на разных страницах дает странные результаты. Я подозреваю, что это связано с initialLayoutAttributesForAppearingItemAtIndexPath: а также finalLayoutAttributesForDisappearingItemAtIndexPath:, но я не могу точно определить, в чем проблема.
  • В моем случае, f(n) зависит от количества столбцов и строк каждой страницы. Есть ли способ извлечь эту информацию из UICollectionViewFlowLayoutесли не считать жесткого кодирования сам? Я думал о запросеlayoutAttributesForElementsInRect: с границами представления коллекции и выводя строки и столбцы оттуда, но это также кажется неэффективным.

4 ответа

Я много думал о вашем вопросе и пришел к следующим соображениям:

Создание подкласса FlowLayout представляется наиболее правильным и эффективным способом изменения порядка ячеек и использования анимации макета потока. И ваш подход работает, кроме двух важных вещей:

  1. Допустим, у вас есть представление коллекции только с 2 ячейками, и вы разработали свою страницу так, чтобы она могла содержать 9 ячеек. Первая ячейка будет расположена в верхнем левом углу представления, как в оригинальном макете потока. Однако ваша вторая ячейка должна располагаться в верхней части представления, и у нее есть индексный путь [0, 1]. Порядок переупорядоченного индекса будет [0, 3] (индексный путь исходной ячейки макета потока, которая будет на его месте). И в твоем layoutAttributesForItemAtIndexPath переопределить вы бы отправили сообщение как [super layoutAttributesForItemAtIndexPath:[0, 3]]Вы бы получили nil объект только потому, что есть только 2 ячейки: [0,0] и [0,1]. И это будет проблемой для вашей последней страницы.
  2. Даже если вы можете реализовать поведение подкачки, переопределив targetContentOffsetForProposedContentOffset:withScrollingVelocity: и вручную установить свойства, такие как itemSize, minimumLineSpacing а также minimumInteritemSpacingЭто большая работа, чтобы сделать ваши элементы симметричными, определить границы подкачки и так далее.

Я понимаю, что создание подкласса макета потока готовит много реализации для вас, потому что то, что вы хотите, больше не является макетом потока. Но давайте подумаем об этом вместе. По поводу ваших вопросов:

  • ваш layoutAttributesForElementsInRect: override - это именно то, чем является оригинальная реализация apple, поэтому нет способа упростить ее. Для вашего случая, однако, вы могли бы рассмотреть следующее: если у вас есть 3 строки элементов на странице, и рамка элемента в первой строке пересекает прямоугольную рамку, то (если все элементы имеют одинаковый размер) кадры элементов второй и третьей строки пересечь этот прямоугольник.
  • извини, я не поняла твой второй вопрос
  • в моем случае функция переупорядочения выглядит следующим образом: (a - целое число строк / столбцов на каждой странице, rows = columns)

f (n) = (n% a²) + (a - 1) (строка - ряд) + a²(n / a²); col = (n % a²) % a; строка = (n % a²) / a;

Отвечая на вопрос, макет потока не знает, сколько строк в каждом столбце, поскольку это число может варьироваться от столбца к столбцу в зависимости от размера каждого элемента. Он также может ничего не сказать о количестве столбцов на каждой странице, потому что это зависит от позиции прокрутки и также может варьироваться. Так что нет лучшего способа, чем запрос layoutAttributesForElementsInRect, но это будет включать также клетки, которые видны только частично. Поскольку ваши ячейки равны по размеру, вы можете теоретически выяснить, сколько строк имеет представление вашей коллекции с горизонтальным направлением прокрутки: начав перебирать каждую ячейку, считая их и разбивая, если их frame.origin.x изменения.

Итак, я думаю, у вас есть два варианта для достижения вашей цели:

  1. Подкласс UICollectionViewLayout, Кажется, много работы по внедрению всех этих методов, но это единственный эффективный способ. Вы можете иметь, например, такие свойства, как itemSize, itemsInOneRow, Тогда вы могли бы легко найти формулу для расчета рамки каждого элемента на основе его номера (лучший способ сделать это в prepareLayout и хранить все кадры в массиве, так что вы не можете получить доступ к кадру, который вам нужен в layoutAttributesForItemAtIndexPath). Внедрение layoutAttributesForItemAtIndexPath, layoutAttributesForItemsInRect а также collectionViewContentSize было бы очень просто. В initialLayoutAttributesForAppearingItemAtIndexPath а также finalLayoutAttributesForDisappearingItemAtIndexPath Вы можете просто установить атрибут альфа 0.0. Вот как работают стандартные анимации макета потока. Переопределением targetContentOffsetForProposedContentOffset:withScrollingVelocity: Вы могли бы реализовать "поведение подкачки".

  2. Подумайте о создании представления коллекции с макетом потока, pagingEnabled = YES, горизонтальное направление прокрутки и размер элемента, равный размеру экрана. Один элемент на размер экрана. Для каждой ячейки вы можете установить новое представление коллекции как подпредставление с вертикальной компоновкой потока и тот же источник данных, что и другие представления коллекции, но со смещением. Это очень эффективно, потому что тогда вы повторно используете целые представления коллекции, содержащие блоки из 9 (или чего-то еще) ячеек, вместо того, чтобы использовать каждую ячейку стандартным способом. Все анимации должны работать правильно.

Здесь вы можете скачать пример проекта, используя подход подклассов макета. (#2)

Разве не было бы простым решением иметь 2 представления коллекции со стандартом UICollectionViewFlowLayout?

Или даже лучше: иметь контроллер просмотра страниц с горизонтальной прокруткой, и каждая страница будет представлением коллекции с нормальным макетом потока.

Идея заключается в следующем: в вашем UICollectionViewController -init method Вы создаете второй вид коллекции со смещением рамки вправо на исходную ширину вида коллекции. Затем вы добавляете его как подпредставление в исходное представление коллекции. Чтобы переключаться между представлениями коллекции, просто добавьте распознаватель пролистывания. Для вычисления значений смещения вы можете сохранить исходный кадр представления коллекции в ivar cVFrame, Для идентификации ваших представлений коллекции вы можете использовать теги.

Пример init метод:

CGRect cVFrame = self.collectionView.frame;
UICollectionView *secondView = [[UICollectionView alloc] 
              initWithFrame:CGRectMake(cVFrame.origin.x + cVFrame.size.width, 0, 
                             cVFrame.size.width, cVFrame.size.height) 
       collectionViewLayout:[UICollectionViewFlowLayout new]];
    [secondView setBackgroundColor:[UIColor greenColor]];
    [secondView setTag:1];
    [secondView setDelegate:self];
    [secondView setDataSource:self];
    [self.collectionView addSubview:secondView];

UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc]
                      initWithTarget:self action:@selector(swipedRight)];
    [swipeRight setDirection:UISwipeGestureRecognizerDirectionRight];

UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] 
                       initWithTarget:self action:@selector(swipedLeft)];
    [swipeLeft setDirection:UISwipeGestureRecognizerDirectionLeft];

    [self.collectionView addGestureRecognizer:swipeRight];
    [self.collectionView addGestureRecognizer:swipeLeft];

Пример swipeRight а также swipeLeft методы:

-(void)swipedRight {
    // Switch to left collection view
    [self.collectionView setContentOffset:CGPointMake(0, 0) animated:YES];
}

-(void)swipedLeft {
    // Switch to right collection view
    [self.collectionView setContentOffset:CGPointMake(cVFrame.size.width, 0) 
                                 animated:YES];
}

И тогда это не большая проблема для реализации методов DataSource (в вашем случае вы хотите иметь 9 элементов на каждой странице):

-(NSInteger)collectionView:(UICollectionView *)collectionView 
    numberOfItemsInSection:(NSInteger)section {
    if (collectionView.tag == 1) {
         // Second collection view
         return self.dataArray.count % 9;
    } else {
         // Original collection view
         return 9; // Or whatever
}

В методе -collectionView:cellForRowAtIndexPath вам нужно будет получить данные из вашей модели со смещением, если это второй вид коллекции.

Также не забудьте зарегистрировать класс для многоразовой ячейки для просмотра второй коллекции. Вы также можете создать только один распознаватель жестов и распознавать пролистывания влево и вправо. Тебе решать.

Я думаю, теперь это должно работать, попробуйте:-)

У вас есть объект, который реализует UICollectionViewDataSource протокол. внутри collectionView:cellForItemAtIndexPath: просто верните правильный товар, который вы хотите вернуть. Я не понимаю, где будет проблема.

Изменить: хорошо, я вижу проблему. Вот решение: http://www.skeuo.com/uicollectionview-custom-layout-tutorial, в частности шаги с 17 по 25. Это не огромный объем работы, и его можно очень легко повторно использовать.

Это мое решение, я не тестировал анимацию, но думаю, что оно будет хорошо работать с небольшими изменениями, надеюсь, это поможет

CELLS_PER_ROW = 4;
CELLS_PER_COLUMN = 5;
CELLS_PER_PAGE = CELLS_PER_ROW * CELLS_PER_COLUMN;

UICollectionViewFlowLayout* flowLayout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout;
flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
flowLayout.itemSize = new CGSize (size.width / CELLS_PER_ROW - delta, size.height / CELLS_PER_COLUMN - delta);
flowLayout.minimumInteritemSpacing = ..;
flowLayout.minimumLineSpacing = ..;
..

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
                  cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSInteger index = indexPath.row;
    NSInteger page = index / CELLS_PER_PAGE;
    NSInteger indexInPage = index - page * CELLS_PER_PAGE;
    NSInteger row = indexInPage % CELLS_PER_COLUMN;
    NSInteger column = indexInPage / CELLS_PER_COLUMN;

    NSInteger dataIndex = row * CELLS_PER_ROW + column + page * CELLS_PER_PAGE;

    id cellData = _data[dataIndex];
    ...
}
Другие вопросы по тегам