Бөлісу құралы:


Представления коллекции в Xamarin.iOS

Представления коллекции позволяют отображать содержимое с помощью произвольных макетов. Они позволяют легко создавать макеты сетки из поля, а также поддерживать пользовательские макеты.

Представления коллекции, доступные в классе, представляют собой новую концепцию в UICollectionView iOS 6, которая представляет несколько элементов на экране с помощью макетов. Шаблоны предоставления данных UICollectionView для создания элементов и взаимодействия с этими элементами следуют тем же шаблонам делегирования и источника данных, которые часто используются в разработке iOS.

Однако представления коллекции работают с подсистемой макета, независимо от UICollectionView самой себя. Поэтому простое предоставление другого макета может легко изменить представление представления коллекции.

iOS предоставляет класс макета, который называется UICollectionViewFlowLayout , что позволяет создавать макеты на основе строк, например сетку без дополнительной работы. Кроме того, можно также создавать пользовательские макеты, позволяющие представить любую презентацию.

Основы UICollectionView

Класс UICollectionView состоит из трех разных элементов:

  • Ячейки — представления на основе данных для каждого элемента
  • Дополнительные представления — представления, управляемые данными, связанные с разделом.
  • Представления оформления — представления, не управляемые данными, созданные макетом

Cells

Ячейки — это объекты, представляющие один элемент в наборе данных, который представлен представлением коллекции. Каждая ячейка является экземпляром UICollectionViewCell класса, который состоит из трех различных представлений, как показано на рисунке ниже:

Каждая ячейка состоит из трех различных представлений, как показано здесь

Класс UICollectionViewCell имеет следующие свойства для каждого из этих представлений:

  • ContentView — это представление содержит содержимое, которое представляет ячейка. Он отображается в самом верхнем порядке z-порядка на экране.
  • SelectedBackgroundView — Ячейки имеют встроенную поддержку выбора. Это представление используется для визуального обозначения выбранной ячейки. Он отображается непосредственно под ContentView выбранным ячейкой.
  • BackgroundView — Ячейки также могут отображать фон, представленный элементом BackgroundView . Это представление отрисовывается под элементом SelectedBackgroundView .

Задав такой ContentView размер, что он меньше BackgroundView и SelectedBackgroundView, BackgroundView его можно использовать для визуального кадра содержимого, в то время SelectedBackgroundView как оно будет отображаться при выборе ячейки, как показано ниже:

Различные элементы ячейки

Ячейки на снимке экрана выше создаются путем наследования от UICollectionViewCell и BackgroundView задания ContentViewSelectedBackgroundView свойств, соответственно, как показано в следующем коде:

public class AnimalCell : UICollectionViewCell
{
        UIImageView imageView;

        [Export ("initWithFrame:")]
        public AnimalCell (CGRect frame) : base (frame)
        {
            BackgroundView = new UIView{BackgroundColor = UIColor.Orange};

            SelectedBackgroundView = new UIView{BackgroundColor = UIColor.Green};

            ContentView.Layer.BorderColor = UIColor.LightGray.CGColor;
            ContentView.Layer.BorderWidth = 2.0f;
            ContentView.BackgroundColor = UIColor.White;
            ContentView.Transform = CGAffineTransform.MakeScale (0.8f, 0.8f);

            imageView = new UIImageView (UIImage.FromBundle ("placeholder.png"));
            imageView.Center = ContentView.Center;
            imageView.Transform = CGAffineTransform.MakeScale (0.7f, 0.7f);

            ContentView.AddSubview (imageView);
        }

        public UIImage Image {
            set {
                imageView.Image = value;
            }
        }
}

Дополнительные представления

Дополнительные представления — это представления, которые представляют информацию, связанную с каждым разделом a UICollectionView. Как и ячейки, дополнительные представления управляются данными. Где ячейки представляют данные элемента из источника данных, дополнительные представления представляют данные раздела, такие как категории книги в книжной полке или жанр музыки в музыкальной библиотеке.

Например, дополнительное представление можно использовать для представления заголовка для определенного раздела, как показано на рисунке ниже:

Дополнительное представление, используемое для представления заголовка для определенного раздела, как показано здесь

Чтобы использовать дополнительное представление, сначала его необходимо зарегистрировать в методе ViewDidLoad :

CollectionView.RegisterClassForSupplementaryView (typeof(Header), UICollectionElementKindSection.Header, headerId);

Затем представление должно быть возвращено с помощью , созданного с GetViewForSupplementaryElementпомощью и DequeueReusableSupplementaryViewнаследуемого.UICollectionReusableView В следующем фрагменте кода будет получен дополнительный просмотр, показанный на снимке экрана выше:

public override UICollectionReusableView GetViewForSupplementaryElement (UICollectionView collectionView, NSString elementKind, NSIndexPath indexPath)
        {
            var headerView = (Header)collectionView.DequeueReusableSupplementaryView (elementKind, headerId, indexPath);
            headerView.Text = "Supplementary View";
            return headerView;
        }

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

Представления оформления

Представления декорирования — это чисто визуальные представления, которые можно отобразить в объекте UICollectionView. В отличие от ячеек и дополнительных представлений, они не управляются данными. Они всегда создаются в подклассе макета и впоследствии могут изменяться в качестве макета содержимого. Например, представление украшения можно использовать для представления фонового представления, которое прокручивает содержимое в списке UICollectionView, как показано ниже:

Представление украшения с красным фоном

Приведенный ниже фрагмент кода изменяет фон на красный в классе примеров CircleLayout :

public class MyDecorationView : UICollectionReusableView
 {
   [Export ("initWithFrame:")]
   public MyDecorationView (CGRect frame) : base (frame)
   {
     BackgroundColor = UIColor.Red;
   }
 }

Источник данных

Как и в других частях iOS, таких как UITableView и MKMapView, UICollectionView получает данные из источника данных, который предоставляется в Xamarin.iOS через UICollectionViewDataSource класс. Этот класс отвечает за предоставление содержимого UICollectionView таким образом:

  • Ячейки — возвращены из GetCell метода.
  • Дополнительные представления — возвращается из GetViewForSupplementaryElement метода.
  • Количество разделов — возвращено из NumberOfSections метода. Значение по умолчанию — 1, если оно не реализовано.
  • Количество элементов в разделе — возвращено из GetItemsCount метода.

UICollectionViewController

Для удобства UICollectionViewController доступен класс. Это автоматически настраивается как делегат, который обсуждается в следующем разделе, так и источник данных для его UICollectionView представления.

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

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

Повторное использование ячеек было упрощено с UICollectionView и UITableView. Вы больше не должны создавать ячейку непосредственно в источнике данных, если он недоступен в очереди повторного использования, так как ячейки регистрируются в системе. Если ячейка недоступна при вызове отмены очереди ячейки из очереди повторного использования, iOS создаст ее автоматически на основе типа или nib, зарегистрированного. Этот же метод также доступен для дополнительных представлений.

Например, рассмотрим следующий код, который регистрирует AnimalCell класс:

static NSString animalCellId = new NSString ("AnimalCell");
CollectionView.RegisterClassForCell (typeof(AnimalCell), animalCellId);

Если требуется ячейка UICollectionView , так как его элемент находится на экране, UICollectionView вызывает метод источника GetCell данных. Аналогично тому, как это работает с UITableView, этот метод отвечает за настройку ячейки из резервных данных, которая будет классом AnimalCell в этом случае.

В следующем коде показана реализация GetCell , которая возвращает AnimalCell экземпляр:

public override UICollectionViewCell GetCell (UICollectionView collectionView, Foundation.NSIndexPath indexPath)
{
        var animalCell = (AnimalCell)collectionView.DequeueReusableCell (animalCellId, indexPath);

        var animal = animals [indexPath.Row];

        animalCell.Image = animal.Image;

        return animalCell;
}

Вызов DequeReusableCell — место, в котором ячейка будет удалена из очереди повторного использования или, если ячейка недоступна в очереди, созданная на основе типа, зарегистрированного в вызове CollectionView.RegisterClassForCell.

В этом случае, зарегистрируя AnimalCell класс, iOS создаст новый AnimalCell внутренне и возвращает его при вызове отмены очереди ячейки, после чего она настроена с изображением, содержащимся в классе животных, и возвращается для отображения в ячейке UICollectionView.

Делегирование

Класс UICollectionView использует делегат типа UICollectionViewDelegate для поддержки взаимодействия с содержимым в .UICollectionView Это позволяет контролировать следующие функции:

  • Выбор ячейки — определяет, выбрана ли ячейка.
  • Выделение ячеек — определяет, касается ли ячейка в настоящее время.
  • Меню ячеек — меню, отображаемое для ячейки в ответ на длинный жест нажатия.

Как и в случае с источником данных, UICollectionViewController параметр по умолчанию настраивается как делегат для источника UICollectionViewданных.

Ячейка HighLighting

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

На этом рисунке показано выделенное состояние непосредственно перед выделением

Для реализации выделения ItemHighlighted можно использовать методы и ItemUnhighlighted методы.UICollectionViewDelegate Например, следующий код будет применять желтый фон выделенного ContentView ячейки и белый фон при отмене выделения, как показано на рисунке выше:

public override void ItemHighlighted (UICollectionView collectionView, NSIndexPath indexPath)
{
        var cell = collectionView.CellForItem(indexPath);
        cell.ContentView.BackgroundColor = UIColor.Yellow;
}

public override void ItemUnhighlighted (UICollectionView collectionView, NSIndexPath indexPath)
{
        var cell = collectionView.CellForItem(indexPath);
        cell.ContentView.BackgroundColor = UIColor.White;
}

Отключение выделения

Выбор включен по умолчанию UICollectionView. Чтобы отключить выделение, переопределите ShouldHighlightItem и верните значение false, как показано ниже:

public override bool ShouldHighlightItem (UICollectionView collectionView, NSIndexPath indexPath)
{
        return false;
}

При отключении выделения процесс выбора ячейки также отключен. Кроме того, существует также метод, который управляет выделением напрямую ShouldSelectItem , хотя ShouldHighlightItem при реализации и возврате false ShouldSelectItem не вызывается.

ShouldSelectItem позволяет включить или отключить выбор на основе элемента, если ShouldHighlightItem он не реализован. Он также позволяет выделять без выделения, если ShouldHighlightItem реализуется и возвращает значение true, а возвращает ShouldSelectItem значение false.

Меню ячеек

Каждая ячейка в ней UICollectionView может отображать меню, позволяющее вырезать, копировать и вставлять необязательно. Чтобы создать меню редактирования в ячейке:

  1. Переопределите ShouldShowMenu и верните значение true, если элемент должен отображать меню.
  2. Переопределите CanPerformAction и верните значение true для каждого действия, которое может выполнять элемент, который будет вырезать, копировать или вставлять.
  3. Переопределите PerformAction для выполнения изменения, копирования операции вставки.

На следующем снимка экрана показано меню, когда ячейка долго нажимается:

На этом снимка экрана показано меню при длительном нажатии ячейки

Макет

UICollectionView поддерживает систему макета, которая позволяет размещать все его элементы, ячейки, дополнительные представления и представления украшения, управлять независимо от UICollectionView самого себя. С помощью системы макетов приложение может поддерживать такие макеты, как сетка, которую мы видели в этой статье, а также предоставлять пользовательские макеты.

Основы макета

Макеты в объекте UICollectionView определяются в классе, наследуемом от UICollectionViewLayout. Реализация макета отвечает за создание атрибутов макета для каждого элемента в объекте UICollectionView. Существует два способа создания макета:

  • Используйте встроенный UICollectionViewFlowLayout .
  • Укажите пользовательский макет, наследуя от UICollectionViewLayout .

Потоковый макет

Класс UICollectionViewFlowLayout предоставляет макет на основе строк, подходящий для упорядочивание содержимого в сетке ячеек, как мы видели.

Чтобы использовать макет потока, выполните приведенные действия.

  • Создание экземпляра UICollectionViewFlowLayout :
var layout = new UICollectionViewFlowLayout ();
  • Передайте экземпляр конструктору UICollectionView :
simpleCollectionViewController = new SimpleCollectionViewController (layout);

Это все, что необходимо для макета содержимого в сетке. Кроме того, когда ориентация изменяется, UICollectionViewFlowLayout дескриптор перестраивает содержимое соответствующим образом, как показано ниже:

Пример изменений ориентации

Набор разделов

Для обеспечения некоторого пространства вокруг UIContentViewмакетов есть SectionInset свойство типа UIEdgeInsets. Например, следующий код предоставляет 50-пиксельный буфер вокруг каждого раздела UIContentView при указании UICollectionViewFlowLayout:

var layout = new UICollectionViewFlowLayout ();
layout.SectionInset = new UIEdgeInsets (50,50,50,50);

Это приводит к интервалу по разделу, как показано ниже:

Интервал вокруг раздела, как показано здесь

Подклассирование UICollectionViewFlowLayout

В выпуске, используемом UICollectionViewFlowLayout напрямую, его также можно подклассить для дальнейшего настройки макета содержимого вдоль строки. Например, это можно использовать для создания макета, который не упаковывает ячейки в сетку, а создает одну строку с горизонтальным эффектом прокрутки, как показано ниже:

Одна строка с горизонтальным эффектом прокрутки

Для реализации этого требуется подкласс UICollectionViewFlowLayout :

  • Инициализация всех свойств макета, применяемых к самому макету или всем элементам в макете конструктора.
  • Переопределение , возвращающее ShouldInvalidateLayoutForBoundsChange значение true, чтобы при границах UICollectionView изменений макет ячеек будет пересчитываться. Это используется в этом случае, чтобы код для преобразования, примененного к самой центровой ячейке, применялся во время прокрутки.
  • Переопределение TargetContentOffset , чтобы сделать самую центральную ячейку привязкой к центру UICollectionView как остановки прокрутки.
  • Переопределение LayoutAttributesForElementsInRect для возврата массива UICollectionViewLayoutAttributes . Каждый UICollectionViewLayoutAttribute содержит сведения о том, как макетировать определенный элемент, включая свойства, такие как его Center , SizeZIndex и Transform3D .

В следующем коде показана такая реализация:

using System;
using CoreGraphics;
using Foundation;
using UIKit;
using CoreGraphics;
using CoreAnimation;

namespace SimpleCollectionView
{
  public class LineLayout : UICollectionViewFlowLayout
  {
    public const float ITEM_SIZE = 200.0f;
    public const int ACTIVE_DISTANCE = 200;
    public const float ZOOM_FACTOR = 0.3f;

    public LineLayout ()
    {
      ItemSize = new CGSize (ITEM_SIZE, ITEM_SIZE);
      ScrollDirection = UICollectionViewScrollDirection.Horizontal;
            SectionInset = new UIEdgeInsets (400,0,400,0);
      MinimumLineSpacing = 50.0f;
    }

    public override bool ShouldInvalidateLayoutForBoundsChange (CGRect newBounds)
    {
      return true;
    }

    public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect (CGRect rect)
    {
      var array = base.LayoutAttributesForElementsInRect (rect);
            var visibleRect = new CGRect (CollectionView.ContentOffset, CollectionView.Bounds.Size);

      foreach (var attributes in array) {
        if (attributes.Frame.IntersectsWith (rect)) {
          float distance = (float)(visibleRect.GetMidX () - attributes.Center.X);
          float normalizedDistance = distance / ACTIVE_DISTANCE;
          if (Math.Abs (distance) < ACTIVE_DISTANCE) {
            float zoom = 1 + ZOOM_FACTOR * (1 - Math.Abs (normalizedDistance));
            attributes.Transform3D = CATransform3D.MakeScale (zoom, zoom, 1.0f);
            attributes.ZIndex = 1;
          }
        }
      }
      return array;
    }

    public override CGPoint TargetContentOffset (CGPoint proposedContentOffset, CGPoint scrollingVelocity)
    {
      float offSetAdjustment = float.MaxValue;
      float horizontalCenter = (float)(proposedContentOffset.X + (this.CollectionView.Bounds.Size.Width / 2.0));
      CGRect targetRect = new CGRect (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 = (float)layoutAttributes.Center.X;
        if (Math.Abs (itemHorizontalCenter - horizontalCenter) < Math.Abs (offSetAdjustment)) {
          offSetAdjustment = itemHorizontalCenter - horizontalCenter;
        }
      }
            return new CGPoint (proposedContentOffset.X + offSetAdjustment, proposedContentOffset.Y);
    }

  }
}

Пользовательский макет

Помимо использования UICollectionViewFlowLayoutмакеты также можно полностью настроить, наследуя непосредственно от UICollectionViewLayout.

Основные методы переопределения:

  • PrepareLayout — используется для выполнения начальных геометрических вычислений, которые будут использоваться во время процесса макета.
  • CollectionViewContentSize — возвращает размер области, используемой для отображения содержимого.
  • LayoutAttributesForElementsInRect — Как и в примере UICollectionViewFlowLayout, этот метод используется для предоставления сведений UICollectionView о том, как макетировать каждый элемент. Тем не менее, в отличие от UICollectionViewFlowLayout пользовательского макета, вы можете разместить элементы, однако вы выберете его.

Например, то же содержимое может быть представлено в круговом макете, как показано ниже:

Циклический пользовательский макет, как показано здесь

Мощные сведения о макетах заключается в том, что для изменения нужно изменить только класс макета, аналогичного сетке, на горизонтальный макет прокрутки, а затем к этому круглому макету требуется только класс макета, предоставленный для UICollectionView изменения. Ни в чем не изменяется делегат или исходный UICollectionViewкод данных.

Изменения в iOS 9

В iOS 9 представление коллекции (UICollectionView) теперь поддерживает переупорядочение элементов из поля, добавив новый распознаватель жестов по умолчанию и несколько новых вспомогательных методов.

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

Пример процесса переупорядочения

В этой статье мы рассмотрим реализацию перетаскивания к переупорядочению в приложении Xamarin.iOS, а также некоторые другие изменения, внесенные в элемент управления представлением коллекции iOS 9:

Изменение порядка элементов

Как упоминалось выше, одним из самых значительных изменений в представлении коллекции в iOS 9 было добавление простой функции перетаскивания к переупорядочению из поля.

В iOS 9 самый быстрый способ добавления переупорядочения в представление коллекции — использовать .UICollectionViewController Теперь контроллер представления коллекции имеет свойство, которое добавляет стандартный InstallsStandardGestureForInteractiveMovementраспознаватель жестов, который поддерживает перетаскивание в элементы переупорядочения в коллекции. Так как значение по умолчанию имеет значение true, необходимо реализовать MoveItem только метод класса для поддержки UICollectionViewDataSource перетаскивания к переупорядочению. Например:

public override void MoveItem (UICollectionView collectionView, NSIndexPath sourceIndexPath, NSIndexPath destinationIndexPath)
{
  // Reorder our list of items
  ...
}

Простой пример переупорядочения

В качестве краткого примера запустите новый проект Xamarin.iOS и измените файл Main.storyboard . Перетащите фигуру UICollectionViewController на поверхность конструктора:

Добавление UICollectionViewController

Выберите представление коллекции (это может быть проще всего сделать из структуры документа). На вкладке макета панели свойств задайте следующие размеры, как показано на снимке экрана ниже:

  • Размер ячейки: ширина — 60 | Высота — 60
  • Размер заголовка: ширина – 0 | Высота — 0
  • Размер нижнего колонтитула: ширина – 0 | Высота — 0
  • Минимальное интервалы: для ячеек – 8 | Для строк — 8
  • Наборы разделов: top – 16 | Внизу – 16 | Слева – 16 | Справа – 16

Настройка размеров представления коллекции

Затем измените ячейку по умолчанию:

  • Изменение цвета фона на синий
  • Добавление метки для действия в качестве заголовка ячейки
  • Задайте для ячейки идентификатор повторного использования

Изменение ячейки по умолчанию

Добавьте ограничения, чтобы сохранить метку по центру внутри ячейки по мере изменения размера:

На панели свойств для CollectionViewCell и задайте для классаTextCollectionViewCellзначение :

Задайте для класса TextCollectionViewCell

Задайте для повторного использования коллекцииCellзначение :

Задайте для коллекции для повторного использования представление ячейки

Наконец, выберите метку и назовите ее TextLabel:

name label TextLabel

Измените TextCollectionViewCell класс и добавьте следующие свойства.

using System;
using Foundation;
using UIKit;

namespace CollectionView
{
  public partial class TextCollectionViewCell : UICollectionViewCell
  {
    #region Computed Properties
    public string Title {
      get { return TextLabel.Text; }
      set { TextLabel.Text = value; }
    }
    #endregion

    #region Constructors
    public TextCollectionViewCell (IntPtr handle) : base (handle)
    {
    }
    #endregion
  }
}

Text Здесь свойство метки предоставляется в качестве заголовка ячейки, поэтому его можно задать из кода.

Добавьте новый класс C# в проект и вызовите его WaterfallCollectionSource. Измените файл и сделайте его следующим образом:

using System;
using Foundation;
using UIKit;
using System.Collections.Generic;

namespace CollectionView
{
  public class WaterfallCollectionSource : UICollectionViewDataSource
  {
    #region Computed Properties
    public WaterfallCollectionView CollectionView { get; set;}
    public List<int> Numbers { get; set; } = new List<int> ();
    #endregion

    #region Constructors
    public WaterfallCollectionSource (WaterfallCollectionView collectionView)
    {
      // Initialize
      CollectionView = collectionView;

      // Init numbers collection
      for (int n = 0; n < 100; ++n) {
        Numbers.Add (n);
      }
    }
    #endregion

    #region Override Methods
    public override nint NumberOfSections (UICollectionView collectionView) {
      // We only have one section
      return 1;
    }

    public override nint GetItemsCount (UICollectionView collectionView, nint section) {
      // Return the number of items
      return Numbers.Count;
    }

    public override UICollectionViewCell GetCell (UICollectionView collectionView, NSIndexPath indexPath)
    {
      // Get a reusable cell and set {~~it's~>its~~} title from the item
      var cell = collectionView.DequeueReusableCell ("Cell", indexPath) as TextCollectionViewCell;
      cell.Title = Numbers [(int)indexPath.Item].ToString();

      return cell;
    }

    public override bool CanMoveItem (UICollectionView collectionView, NSIndexPath indexPath) {
      // We can always move items
      return true;
    }

    public override void MoveItem (UICollectionView collectionView, NSIndexPath sourceIndexPath, NSIndexPath destinationIndexPath)
    {
      // Reorder our list of items
      var item = Numbers [(int)sourceIndexPath.Item];
      Numbers.RemoveAt ((int)sourceIndexPath.Item);
      Numbers.Insert ((int)destinationIndexPath.Item, item);
    }
    #endregion
  }
}

Этот класс будет источником данных для представления коллекции и предоставлять сведения для каждой ячейки в коллекции. Обратите внимание, что MoveItem метод реализован для разрешения перетаскивания элементов в коллекции.

Добавьте в проект еще один новый класс C# и вызовите его WaterfallCollectionDelegate. Измените этот файл и сделайте его следующим образом:

using System;
using Foundation;
using UIKit;
using System.Collections.Generic;

namespace CollectionView
{
  public class WaterfallCollectionDelegate : UICollectionViewDelegate
  {
    #region Computed Properties
    public WaterfallCollectionView CollectionView { get; set;}
    #endregion

    #region Constructors
    public WaterfallCollectionDelegate (WaterfallCollectionView collectionView)
    {

      // Initialize
      CollectionView = collectionView;

    }
    #endregion

    #region Overrides Methods
    public override bool ShouldHighlightItem (UICollectionView collectionView, NSIndexPath indexPath) {
      // Always allow for highlighting
      return true;
    }

    public override void ItemHighlighted (UICollectionView collectionView, NSIndexPath indexPath)
    {
      // Get cell and change to green background
      var cell = collectionView.CellForItem(indexPath);
      cell.ContentView.BackgroundColor = UIColor.FromRGB(183,208,57);
    }

    public override void ItemUnhighlighted (UICollectionView collectionView, NSIndexPath indexPath)
    {
      // Get cell and return to blue background
      var cell = collectionView.CellForItem(indexPath);
      cell.ContentView.BackgroundColor = UIColor.FromRGB(164,205,255);
    }
    #endregion
  }
}

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

Добавьте в проект последний класс C# и вызовите его WaterfallCollectionView. Измените этот файл и сделайте его следующим образом:

using System;
using UIKit;
using System.Collections.Generic;
using Foundation;

namespace CollectionView
{
  [Register("WaterfallCollectionView")]
  public class WaterfallCollectionView : UICollectionView
  {

    #region Constructors
    public WaterfallCollectionView (IntPtr handle) : base (handle)
    {
    }
    #endregion

    #region Override Methods
    public override void AwakeFromNib ()
    {
      base.AwakeFromNib ();

      // Initialize
      DataSource = new WaterfallCollectionSource(this);
      Delegate = new WaterfallCollectionDelegate(this);

    }
    #endregion
  }
}

Обратите внимание, что и Delegate что DataSource мы создали выше, когда представление коллекции создается из раскадровки (или XIB-файла).

Измените файл Main.storyboard еще раз и выберите представление коллекции и переключитесь на свойства. Задайте класс пользовательскому WaterfallCollectionView классу, который мы определили выше:

Сохраните изменения, внесенные в пользовательский интерфейс, и запустите приложение. Если пользователь выбирает элемент из списка и перетаскивает его в новое расположение, другие элементы автоматически будут анимироваться при переходе из пути элемента. Когда пользователь удаляет элемент в новом расположении, он будет придерживаться этого расположения. Например:

Пример перетаскивания элемента в новое расположение

Использование пользовательского распознавателя жестов

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

public override void ViewDidLoad ()
{
  base.ViewDidLoad ();

  // Create a custom gesture recognizer
  var longPressGesture = new UILongPressGestureRecognizer ((gesture) => {

    // Take action based on state
    switch(gesture.State) {
    case UIGestureRecognizerState.Began:
      var selectedIndexPath = CollectionView.IndexPathForItemAtPoint(gesture.LocationInView(View));
      if (selectedIndexPath !=null) {
        CollectionView.BeginInteractiveMovementForItem(selectedIndexPath);
      }
      break;
    case UIGestureRecognizerState.Changed:
      CollectionView.UpdateInteractiveMovementTargetPosition(gesture.LocationInView(View));
      break;
    case UIGestureRecognizerState.Ended:
      CollectionView.EndInteractiveMovement();
      break;
    default:
      CollectionView.CancelInteractiveMovement();
      break;
    }

  });

  // Add the custom recognizer to the collection view
  CollectionView.AddGestureRecognizer(longPressGesture);
}

Здесь мы используем несколько новых методов, добавленных в представление коллекции для реализации и управления операцией перетаскивания:

  • BeginInteractiveMovementForItem — помечает начало операции перемещения.
  • UpdateInteractiveMovementTargetPosition — Отправляется по мере обновления расположения элемента.
  • EndInteractiveMovement — помечает конец перемещения элемента.
  • CancelInteractiveMovement — помечает пользователя, отменяющего операцию перемещения.

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

Пользовательские макеты и изменение порядка

В iOS 9 добавлены несколько новых методов для работы с переупорядочением перетаскивания и пользовательскими макетами в представлении коллекции. Чтобы изучить эту функцию, давайте добавим в коллекцию пользовательский макет.

Сначала добавьте в проект новый класс WaterfallCollectionLayout C#. Измените его и сделайте его следующим образом:

using System;
using Foundation;
using UIKit;
using System.Collections.Generic;
using CoreGraphics;

namespace CollectionView
{
  [Register("WaterfallCollectionLayout")]
  public class WaterfallCollectionLayout : UICollectionViewLayout
  {
    #region Private Variables
    private int columnCount = 2;
    private nfloat minimumColumnSpacing = 10;
    private nfloat minimumInterItemSpacing = 10;
    private nfloat headerHeight = 0.0f;
    private nfloat footerHeight = 0.0f;
    private UIEdgeInsets sectionInset = new UIEdgeInsets(0, 0, 0, 0);
    private WaterfallCollectionRenderDirection itemRenderDirection = WaterfallCollectionRenderDirection.ShortestFirst;
    private Dictionary<nint,UICollectionViewLayoutAttributes> headersAttributes = new Dictionary<nint, UICollectionViewLayoutAttributes>();
    private Dictionary<nint,UICollectionViewLayoutAttributes> footersAttributes = new Dictionary<nint, UICollectionViewLayoutAttributes>();
    private List<CGRect> unionRects = new List<CGRect>();
    private List<nfloat> columnHeights = new List<nfloat>();
    private List<UICollectionViewLayoutAttributes> allItemAttributes = new List<UICollectionViewLayoutAttributes>();
    private List<List<UICollectionViewLayoutAttributes>> sectionItemAttributes = new List<List<UICollectionViewLayoutAttributes>>();
    private nfloat unionSize = 20;
    #endregion

    #region Computed Properties
    [Export("ColumnCount")]
    public int ColumnCount {
      get { return columnCount; }
      set {
        WillChangeValue ("ColumnCount");
        columnCount = value;
        DidChangeValue ("ColumnCount");

        InvalidateLayout ();
      }
    }

    [Export("MinimumColumnSpacing")]
    public nfloat MinimumColumnSpacing {
      get { return minimumColumnSpacing; }
      set {
        WillChangeValue ("MinimumColumnSpacing");
        minimumColumnSpacing = value;
        DidChangeValue ("MinimumColumnSpacing");

        InvalidateLayout ();
      }
    }

    [Export("MinimumInterItemSpacing")]
    public nfloat MinimumInterItemSpacing {
      get { return minimumInterItemSpacing; }
      set {
        WillChangeValue ("MinimumInterItemSpacing");
        minimumInterItemSpacing = value;
        DidChangeValue ("MinimumInterItemSpacing");

        InvalidateLayout ();
      }
    }

    [Export("HeaderHeight")]
    public nfloat HeaderHeight {
      get { return headerHeight; }
      set {
        WillChangeValue ("HeaderHeight");
        headerHeight = value;
        DidChangeValue ("HeaderHeight");

        InvalidateLayout ();
      }
    }

    [Export("FooterHeight")]
    public nfloat FooterHeight {
      get { return footerHeight; }
      set {
        WillChangeValue ("FooterHeight");
        footerHeight = value;
        DidChangeValue ("FooterHeight");

        InvalidateLayout ();
      }
    }

    [Export("SectionInset")]
    public UIEdgeInsets SectionInset {
      get { return sectionInset; }
      set {
        WillChangeValue ("SectionInset");
        sectionInset = value;
        DidChangeValue ("SectionInset");

        InvalidateLayout ();
      }
    }

    [Export("ItemRenderDirection")]
    public WaterfallCollectionRenderDirection ItemRenderDirection {
      get { return itemRenderDirection; }
      set {
        WillChangeValue ("ItemRenderDirection");
        itemRenderDirection = value;
        DidChangeValue ("ItemRenderDirection");

        InvalidateLayout ();
      }
    }
    #endregion

    #region Constructors
    public WaterfallCollectionLayout ()
    {
    }

    public WaterfallCollectionLayout(NSCoder coder) : base(coder) {

    }
    #endregion

    #region Public Methods
    public nfloat ItemWidthInSectionAtIndex(int section) {

      var width = CollectionView.Bounds.Width - SectionInset.Left - SectionInset.Right;
      return (nfloat)Math.Floor ((width - ((ColumnCount - 1) * MinimumColumnSpacing)) / ColumnCount);
    }
    #endregion

    #region Override Methods
    public override void PrepareLayout ()
    {
      base.PrepareLayout ();

      // Get the number of sections
      var numberofSections = CollectionView.NumberOfSections();
      if (numberofSections == 0)
        return;

      // Reset collections
      headersAttributes.Clear ();
      footersAttributes.Clear ();
      unionRects.Clear ();
      columnHeights.Clear ();
      allItemAttributes.Clear ();
      sectionItemAttributes.Clear ();

      // Initialize column heights
      for (int n = 0; n < ColumnCount; n++) {
        columnHeights.Add ((nfloat)0);
      }

      // Process all sections
      nfloat top = 0.0f;
      var attributes = new UICollectionViewLayoutAttributes ();
      var columnIndex = 0;
      for (nint section = 0; section < numberofSections; ++section) {
        // Calculate section specific metrics
        var minimumInterItemSpacing = (MinimumInterItemSpacingForSection == null) ? MinimumColumnSpacing :
          MinimumInterItemSpacingForSection (CollectionView, this, section);

        // Calculate widths
        var width = CollectionView.Bounds.Width - SectionInset.Left - SectionInset.Right;
        var itemWidth = (nfloat)Math.Floor ((width - ((ColumnCount - 1) * MinimumColumnSpacing)) / ColumnCount);

        // Calculate section header
        var heightHeader = (HeightForHeader == null) ? HeaderHeight :
          HeightForHeader (CollectionView, this, section);

        if (heightHeader > 0) {
          attributes = UICollectionViewLayoutAttributes.CreateForSupplementaryView (UICollectionElementKindSection.Header, NSIndexPath.FromRowSection (0, section));
          attributes.Frame = new CGRect (0, top, CollectionView.Bounds.Width, heightHeader);
          headersAttributes.Add (section, attributes);
          allItemAttributes.Add (attributes);

          top = attributes.Frame.GetMaxY ();
        }

        top += SectionInset.Top;
        for (int n = 0; n < ColumnCount; n++) {
          columnHeights [n] = top;
        }

        // Calculate Section Items
        var itemCount = CollectionView.NumberOfItemsInSection(section);
        List<UICollectionViewLayoutAttributes> itemAttributes = new List<UICollectionViewLayoutAttributes> ();

        for (nint n = 0; n < itemCount; n++) {
          var indexPath = NSIndexPath.FromRowSection (n, section);
          columnIndex = NextColumnIndexForItem (n);
          var xOffset = SectionInset.Left + (itemWidth + MinimumColumnSpacing) * (nfloat)columnIndex;
          var yOffset = columnHeights [columnIndex];
          var itemSize = (SizeForItem == null) ? new CGSize (0, 0) : SizeForItem (CollectionView, this, indexPath);
          nfloat itemHeight = 0.0f;

          if (itemSize.Height > 0.0f && itemSize.Width > 0.0f) {
            itemHeight = (nfloat)Math.Floor (itemSize.Height * itemWidth / itemSize.Width);
          }

          attributes = UICollectionViewLayoutAttributes.CreateForCell (indexPath);
          attributes.Frame = new CGRect (xOffset, yOffset, itemWidth, itemHeight);
          itemAttributes.Add (attributes);
          allItemAttributes.Add (attributes);
          columnHeights [columnIndex] = attributes.Frame.GetMaxY () + MinimumInterItemSpacing;
        }
        sectionItemAttributes.Add (itemAttributes);

        // Calculate Section Footer
        nfloat footerHeight = 0.0f;
        columnIndex = LongestColumnIndex();
        top = columnHeights [columnIndex] - MinimumInterItemSpacing + SectionInset.Bottom;
        footerHeight = (HeightForFooter == null) ? FooterHeight : HeightForFooter(CollectionView, this, section);

        if (footerHeight > 0) {
          attributes = UICollectionViewLayoutAttributes.CreateForSupplementaryView (UICollectionElementKindSection.Footer, NSIndexPath.FromRowSection (0, section));
          attributes.Frame = new CGRect (0, top, CollectionView.Bounds.Width, footerHeight);
          footersAttributes.Add (section, attributes);
          allItemAttributes.Add (attributes);
          top = attributes.Frame.GetMaxY ();
        }

        for (int n = 0; n < ColumnCount; n++) {
          columnHeights [n] = top;
        }
      }

      var i =0;
      var attrs = allItemAttributes.Count;
      while(i < attrs) {
        var rect1 = allItemAttributes [i].Frame;
        i = (int)Math.Min (i + unionSize, attrs) - 1;
        var rect2 = allItemAttributes [i].Frame;
        unionRects.Add (CGRect.Union (rect1, rect2));
        i++;
      }

    }

    public override CGSize CollectionViewContentSize {
      get {
        if (CollectionView.NumberOfSections () == 0) {
          return new CGSize (0, 0);
        }

        var contentSize = CollectionView.Bounds.Size;
        contentSize.Height = columnHeights [0];
        return contentSize;
      }
    }

    public override UICollectionViewLayoutAttributes LayoutAttributesForItem (NSIndexPath indexPath)
    {
      if (indexPath.Section >= sectionItemAttributes.Count) {
        return null;
      }

      if (indexPath.Item >= sectionItemAttributes [indexPath.Section].Count) {
        return null;
      }

      var list = sectionItemAttributes [indexPath.Section];
      return list [(int)indexPath.Item];
    }

    public override UICollectionViewLayoutAttributes LayoutAttributesForSupplementaryView (NSString kind, NSIndexPath indexPath)
    {
      var attributes = new UICollectionViewLayoutAttributes ();

      switch (kind) {
      case "header":
        attributes = headersAttributes [indexPath.Section];
        break;
      case "footer":
        attributes = footersAttributes [indexPath.Section];
        break;
      }

      return attributes;
    }

    public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect (CGRect rect)
    {
      var begin = 0;
      var end = unionRects.Count;
      List<UICollectionViewLayoutAttributes> attrs = new List<UICollectionViewLayoutAttributes> ();

      for (int i = 0; i < end; i++) {
        if (rect.IntersectsWith(unionRects[i])) {
          begin = i * (int)unionSize;
        }
      }

      for (int i = end - 1; i >= 0; i--) {
        if (rect.IntersectsWith (unionRects [i])) {
          end = (int)Math.Min ((i + 1) * (int)unionSize, allItemAttributes.Count);
          break;
        }
      }

      for (int i = begin; i < end; i++) {
        var attr = allItemAttributes [i];
        if (rect.IntersectsWith (attr.Frame)) {
          attrs.Add (attr);
        }
      }

      return attrs.ToArray();
    }

    public override bool ShouldInvalidateLayoutForBoundsChange (CGRect newBounds)
    {
      var oldBounds = CollectionView.Bounds;
      return (newBounds.Width != oldBounds.Width);
    }
    #endregion

    #region Private Methods
    private int ShortestColumnIndex() {
      var index = 0;
      var shortestHeight = nfloat.MaxValue;
      var n = 0;

      // Scan each column for the shortest height
      foreach (nfloat height in columnHeights) {
        if (height < shortestHeight) {
          shortestHeight = height;
          index = n;
        }
        ++n;
      }

      return index;
    }

    private int LongestColumnIndex() {
      var index = 0;
      var longestHeight = nfloat.MinValue;
      var n = 0;

      // Scan each column for the shortest height
      foreach (nfloat height in columnHeights) {
        if (height > longestHeight) {
          longestHeight = height;
          index = n;
        }
        ++n;
      }

      return index;
    }

    private int NextColumnIndexForItem(nint item) {
      var index = 0;

      switch (ItemRenderDirection) {
      case WaterfallCollectionRenderDirection.ShortestFirst:
        index = ShortestColumnIndex ();
        break;
      case WaterfallCollectionRenderDirection.LeftToRight:
        index = ColumnCount;
        break;
      case WaterfallCollectionRenderDirection.RightToLeft:
        index = (ColumnCount - 1) - ((int)item / ColumnCount);
        break;
      }

      return index;
    }
    #endregion

    #region Events
    public delegate CGSize WaterfallCollectionSizeDelegate(UICollectionView collectionView, WaterfallCollectionLayout layout, NSIndexPath indexPath);
    public delegate nfloat WaterfallCollectionFloatDelegate(UICollectionView collectionView, WaterfallCollectionLayout layout, nint section);
    public delegate UIEdgeInsets WaterfallCollectionEdgeInsetsDelegate(UICollectionView collectionView, WaterfallCollectionLayout layout, nint section);

    public event WaterfallCollectionSizeDelegate SizeForItem;
    public event WaterfallCollectionFloatDelegate HeightForHeader;
    public event WaterfallCollectionFloatDelegate HeightForFooter;
    public event WaterfallCollectionEdgeInsetsDelegate InsetForSection;
    public event WaterfallCollectionFloatDelegate MinimumInterItemSpacingForSection;
    #endregion
  }
}

Этот класс можно использовать для предоставления пользовательского двух столбца, макета каскадного типа в представлении коллекции. Код использует код key-Value (с помощью WillChangeValue методов DidChangeValue ) для предоставления привязки данных для вычисляемых свойств этого класса.

Затем измените WaterfallCollectionSource и внесите следующие изменения и дополнения:

private Random rnd = new Random();
...

public List<nfloat> Heights { get; set; } = new List<nfloat> ();
...

public WaterfallCollectionSource (WaterfallCollectionView collectionView)
{
  // Initialize
  CollectionView = collectionView;

  // Init numbers collection
  for (int n = 0; n < 100; ++n) {
    Numbers.Add (n);
    Heights.Add (rnd.Next (0, 100) + 40.0f);
  }
}

Это приведет к созданию случайной высоты для каждого элемента, который будет отображаться в списке.

Затем измените класс и добавьте следующее WaterfallCollectionView вспомогательное свойство:

public WaterfallCollectionSource Source {
  get { return (WaterfallCollectionSource)DataSource; }
}

Это упрощает получение в источнике данных (и высоты элементов) из пользовательского макета.

Наконец, измените контроллер представления и добавьте следующий код:

public override void AwakeFromNib ()
{
  base.AwakeFromNib ();

  var waterfallLayout = new WaterfallCollectionLayout ();

  // Wireup events
  waterfallLayout.SizeForItem += (collectionView, layout, indexPath) => {
    var collection = collectionView as WaterfallCollectionView;
    return new CGSize((View.Bounds.Width-40)/3,collection.Source.Heights[(int)indexPath.Item]);
  };

  // Attach the custom layout to the collection
  CollectionView.SetCollectionViewLayout(waterfallLayout, false);
}

Это создает экземпляр пользовательского макета, задает событие для предоставления размера каждого элемента и присоединяет новый макет к представлению коллекции.

Если снова запустить приложение Xamarin.iOS, представление коллекции будет выглядеть следующим образом:

Теперь представление коллекции будет выглядеть следующим образом.

Мы по-прежнему можем перетаскивать элементы к переупорядочению, как и раньше, но элементы теперь изменят размер, чтобы соответствовать новому расположению при их удалении.

Изменения представления коллекции

В следующих разделах мы подробно рассмотрим изменения, внесенные в каждый класс в представлении коллекции iOS 9.

UICollectionView

В класс iOS 9 были внесены UICollectionView следующие изменения или дополнения:

  • BeginInteractiveMovementForItem — помечает начало операции перетаскивания.
  • CancelInteractiveMovement — сообщает представлению коллекции, что пользователь отменил операцию перетаскивания.
  • EndInteractiveMovement — сообщает представлению коллекции, что пользователь завершил операцию перетаскивания.
  • GetIndexPathsForVisibleSupplementaryElements — возвращает верхний indexPath или нижний колонтитул в разделе представления коллекции.
  • GetSupplementaryView — возвращает заданный верхний или нижний колонтитул.
  • GetVisibleSupplementaryViews — возвращает список всех видимых верхних и нижних колонтитулов.
  • UpdateInteractiveMovementTargetPosition — сообщает представлению коллекции о перемещении или перемещении элемента во время операции перетаскивания.

UICollectionViewController

В iOS 9 были внесены UICollectionViewController следующие изменения или дополнения:

  • InstallsStandardGestureForInteractiveMovement — Если true будет использоваться новый распознаватель жестов, который автоматически поддерживает переупорядочение перетаскивания.
  • CanMoveItem — сообщает представлению коллекции, если заданный элемент может быть переупорядочен.
  • GetTargetContentOffset — используется для получения смещения заданного элемента представления коллекции.
  • GetTargetIndexPathForMove — получает indexPath заданный элемент для операции перетаскивания.
  • MoveItem — перемещает порядок заданного элемента в списке.

UICollectionViewDataSource

В iOS 9 были внесены UICollectionViewDataSource следующие изменения или дополнения:

  • CanMoveItem — сообщает представлению коллекции, если заданный элемент может быть переупорядочен.
  • MoveItem — перемещает порядок заданного элемента в списке.

UICollectionViewDelegate

В iOS 9 были внесены UICollectionViewDelegate следующие изменения или дополнения:

  • GetTargetContentOffset — используется для получения смещения заданного элемента представления коллекции.
  • GetTargetIndexPathForMove — получает indexPath заданный элемент для операции перетаскивания.

UICollectionViewFlowLayout

В iOS 9 были внесены UICollectionViewFlowLayout следующие изменения или дополнения:

  • SectionFootersPinToVisibleBounds — приклеивает нижние колонтитулы раздела к видимым границам представления коллекции.
  • SectionHeadersPinToVisibleBounds — приклеивает заголовки раздела к видимым границам представления коллекции.

UICollectionViewLayout

В iOS 9 были внесены UICollectionViewLayout следующие изменения или дополнения:

  • GetInvalidationContextForEndingInteractiveMovementOfItems — возвращает контекст недопустимости в конце операции перетаскивания, когда пользователь завершает перетаскивание или отменяет его.
  • GetInvalidationContextForInteractivelyMovingItems — возвращает контекст недопустимости в начале операции перетаскивания.
  • GetLayoutAttributesForInteractivelyMovingItem — получает атрибуты макета для данного элемента при перетаскивании элемента.
  • GetTargetIndexPathForInteractivelyMovingItem — возвращает indexPath элемент, который находится в заданной точке при перетаскивании элемента.

UICollectionViewLayoutAttributes

В iOS 9 были внесены UICollectionViewLayoutAttributes следующие изменения или дополнения:

  • CollisionBoundingPath — возвращает путь столкновения двух элементов во время операции перетаскивания.
  • CollisionBoundsType — возвращает тип столкновения (как) UIDynamicItemCollisionBoundsTypeкоторый произошел во время операции перетаскивания.

UICollectionViewLayoutInvalidationContext

В iOS 9 были внесены UICollectionViewLayoutInvalidationContext следующие изменения или дополнения:

  • InteractiveMovementTarget — возвращает целевой элемент операции перетаскивания.
  • PreviousIndexPathsForInteractivelyMovingItems — возвращает indexPaths другие элементы, участвующие в операции перетаскивания для переупорядочения.
  • TargetIndexPathsForInteractivelyMovingItems — возвращает indexPaths элементы, которые будут переупорядочены в результате операции перетаскивания к переупорядочению.

UICollectionViewSource

В iOS 9 были внесены UICollectionViewSource следующие изменения или дополнения:

  • CanMoveItem — сообщает представлению коллекции, если заданный элемент может быть переупорядочен.
  • GetTargetContentOffset — возвращает смещения элементов, которые будут перемещаться с помощью операции перетаскивания к переупорядочению.
  • GetTargetIndexPathForMove — возвращает indexPath элемент, который будет перемещен во время операции перетаскивания к переупорядочению.
  • MoveItem — перемещает порядок заданного элемента в списке.

Итоги

В этой статье рассматриваются изменения представлений коллекции в iOS 9 и описано, как реализовать их в Xamarin.iOS. Он охватывал реализацию простого действия перетаскивания к переупорядочению в представлении коллекции; использование пользовательского распознавателя жестов с переупорядочением перетаскивания; и способ переупорядочения перетаскивания влияет на макет пользовательского представления коллекции.