Vistas de colección en Xamarin.iOS
Las vistas de colección permiten mostrar contenido mediante diseños arbitrarios. Permiten crear fácilmente diseños tipo cuadrícula desde el primer momento, al tiempo que son compatibles con diseños personalizados.
Las vistas de colección, disponibles en la UICollectionView
clase, son un nuevo concepto en iOS 6 que presentan varios elementos en la pantalla mediante diseños. Los patrones para proporcionar datos a para UICollectionView
crear elementos e interactuar con esos elementos siguen los mismos patrones de delegación y origen de datos que se usan habitualmente en el desarrollo de iOS.
Sin embargo, las vistas de colección funcionan con un subsistema de diseño que es independiente del UICollectionView
propio. Por lo tanto, simplemente proporcionar un diseño diferente puede cambiar fácilmente la presentación de una vista de colección.
iOS proporciona una clase de diseño denominada UICollectionViewFlowLayout
que permite crear diseños basados en líneas como una cuadrícula sin trabajo adicional. Además, también se pueden crear diseños personalizados que permitan cualquier presentación que pueda imaginar.
Conceptos básicos de UICollectionView
La UICollectionView
clase consta de tres elementos diferentes:
- Celdas : vistas controladas por datos para cada elemento
- Vistas complementarias: vistas controladas por datos asociadas a una sección.
- Vistas de decoración: vistas no controladas por datos creadas por un diseño
Celdas
Las celdas son objetos que representan un único elemento del conjunto de datos que presenta la vista de colección. Cada celda es una instancia de la clase UICollectionViewCell
, que se compone de tres vistas diferentes, como se muestra en la ilustración siguiente:
La clase UICollectionViewCell
tiene las siguientes propiedades para cada una de estas vistas:
ContentView
: esta vista contiene el contenido que presenta la celda. Se representa en el orden z superior de la pantalla.SelectedBackgroundView
– Las celdas tienen compatibilidad integrada para la selección. Esta vista se usa para indicar visualmente que se selecciona una celda. Se representa justo debajo de cuandoContentView
se selecciona una celda.BackgroundView
– Las celdas también pueden mostrar un fondo, que presenta .BackgroundView
. Esta vista se representa debajo deSelectedBackgroundView
.
Al establecer el ContentView
de modo que sea menor que BackgroundView
y SelectedBackgroundView
, BackgroundView
se puede usar para enmarcar visualmente el contenido, mientras que SelectedBackgroundView
se mostrará cuando se seleccione una celda, como se muestra a continuación:
Las celdas de la captura de pantalla anterior se crean heredando de UICollectionViewCell
y estableciendo las propiedades ContentView
, SelectedBackgroundView
y BackgroundView
, respectivamente, como se muestra en el código siguiente:
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;
}
}
}
Vistas complementarias
Las vistas complementarias son vistas que presentan información asociada a cada sección de un UICollectionView
. Al igual que las celdas, las vistas complementarias están controladas por datos. Donde Las celdas presentan los datos del elemento de un origen de datos, las vistas complementarias presentan los datos de sección, como las categorías de libro en una estantería o el género de música en una biblioteca de música.
Por ejemplo, se podría usar una vista complementaria para presentar un encabezado para una sección determinada, como se muestra en la ilustración siguiente:
Para usar una vista complementaria, primero debe registrarse en el ViewDidLoad
método :
CollectionView.RegisterClassForSupplementaryView (typeof(Header), UICollectionElementKindSection.Header, headerId);
A continuación, la vista debe devolverse mediante GetViewForSupplementaryElement
, creada mediante DequeueReusableSupplementaryView
y hereda de UICollectionReusableView
. El siguiente fragmento de código generará la SupplementaryView que se muestra en la captura de pantalla anterior:
public override UICollectionReusableView GetViewForSupplementaryElement (UICollectionView collectionView, NSString elementKind, NSIndexPath indexPath)
{
var headerView = (Header)collectionView.DequeueReusableSupplementaryView (elementKind, headerId, indexPath);
headerView.Text = "Supplementary View";
return headerView;
}
Las vistas complementarias son más genéricas que solo encabezados y pies de página. Se pueden colocar en cualquier parte de la vista de colección y se pueden componer de cualquier vista, lo que hace que su apariencia sea totalmente personalizable.
Vistas de decoración
Las vistas de decoración son vistas puramente visuales que se pueden mostrar en un UICollectionView
. A diferencia de las celdas y las vistas complementarias, no están controladas por datos. Siempre se crean dentro de la subclase de un diseño y, posteriormente, pueden cambiar como diseño del contenido. Por ejemplo, se podría usar una vista de decoración para presentar una vista de fondo que se desplaza con el contenido de UICollectionView
, como se muestra a continuación:
El fragmento de código siguiente cambia el fondo a rojo en la clase samples CircleLayout
:
public class MyDecorationView : UICollectionReusableView
{
[Export ("initWithFrame:")]
public MyDecorationView (CGRect frame) : base (frame)
{
BackgroundColor = UIColor.Red;
}
}
Origen de datos
Al igual que con otras partes de iOS, como UITableView
y MKMapView
, UICollectionView
obtiene sus datos de un origen de datos, que se expone en Xamarin.iOS a través de la clase UICollectionViewDataSource
. Esta clase es responsable de proporcionar contenido a UICollectionView
, como:
- Celdas : se devuelve desde el método
GetCell
. - Vistas complementarias: se devuelve desde el método
GetViewForSupplementaryElement
. - Número de secciones: se devuelve desde el método
NumberOfSections
. El valor predeterminado es 1 si no se implementa. - Número de elementos por sección: devuelto desde el método
GetItemsCount
.
UICollectionViewController
Para mayor comodidad, la clase UICollectionViewController
está disponible. Se configura automáticamente para que sea el delegado, que se describe en la sección siguiente y el origen de datos de su vista UICollectionView
.
Al igual que con UITableView
, la clase UICollectionView
solo llamará a su origen de datos para obtener Celdas para los elementos que están en la pantalla.
Las celdas que se desplazan fuera de la pantalla se colocan en una cola para su reutilización, como se muestra en la imagen siguiente:
La reutilización de celdas se ha simplificado con UICollectionView
y UITableView
. Ya no es necesario crear una celda directamente en el origen de datos si no está disponible en la cola de reutilización, ya que las celdas se registran en el sistema. Si una celda no está disponible al realizar la llamada a la desaplicación de la celda de la cola de reutilización, iOS la creará automáticamente en función del tipo o nib que se registró.
La misma técnica también está disponible para vistas complementarias.
Por ejemplo, considere el código siguiente que registra la clase AnimalCell
:
static NSString animalCellId = new NSString ("AnimalCell");
CollectionView.RegisterClassForCell (typeof(AnimalCell), animalCellId);
Cuando una UICollectionView
necesita una celda porque su elemento está en la pantalla, llama al UICollectionView
método del origen de datos GetCell
. De forma similar a cómo funciona con UITableView, este método es responsable de configurar una celda a partir de los datos de respaldo, que sería una AnimalCell
clase en este caso.
El código siguiente muestra una implementación de GetCell
que devuelve una instancia 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;
}
La llamada a DequeReusableCell
es donde la celda se desconecta de la cola de reutilización o, si una celda no está disponible en la cola, creada en función del tipo registrado en la llamada a CollectionView.RegisterClassForCell
.
En este caso, al registrar la AnimalCell
clase, iOS creará un nuevo AnimalCell
internamente y lo devolverá cuando se realice una llamada a la cola de una celda, después de lo cual se configura con la imagen contenida en la clase animal y se devuelve para que se muestre en UICollectionView
.
Delegar
La clase UICollectionView
usa un delegado de tipo UICollectionViewDelegate
para admitir la interacción con el contenido de UICollectionView
. Esto permite el control de:
- Selección de celda: determina si se selecciona una celda.
- Resaltado de celdas: determina si se está tocándose actualmente una celda.
- Menús de celda: menú que se muestra para una celda en respuesta a un gesto de presión larga.
Al igual que con el origen de datos, UICollectionViewController
se configura de forma predeterminada para que sea el delegado de UICollectionView
.
HighLighting de celda
Cuando se presiona una celda, la celda pasa a un estado resaltado y no se selecciona hasta que el usuario levanta el dedo desde la celda. Esto permite un cambio temporal en la apariencia de la celda antes de que se seleccione realmente. Tras la selección, se muestra la celda SelectedBackgroundView
. En la ilustración siguiente se muestra el estado resaltado justo antes de que se produzca la selección:
Para implementar el resaltado, se pueden usar los métodos ItemHighlighted
y ItemUnhighlighted
de UICollectionViewDelegate
. Por ejemplo, el código siguiente aplicará un fondo amarillo de ContentView
cuando la celda esté resaltada y un fondo blanco cuando se desactive, como se muestra en la imagen anterior:
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;
}
Deshabilitación de la selección
La selección está habilitada de forma predeterminada en UICollectionView
. Para deshabilitar la selección, invalide ShouldHighlightItem
y devuelva false como se muestra a continuación:
public override bool ShouldHighlightItem (UICollectionView collectionView, NSIndexPath indexPath)
{
return false;
}
Cuando se deshabilita el resaltado, también se deshabilita el proceso de selección de una celda. Además, también hay un ShouldSelectItem
método que controla la selección directamente, aunque si ShouldHighlightItem
se implementa y regresa false, no se llama a ShouldSelectItem
.
ShouldSelectItem
permite activar o desactivar la selección en un elemento por elemento, cuando ShouldHighlightItem
no se implementa. También permite resaltar sin selección, si ShouldHighlightItem
se implementa y devuelve true, mientras que ShouldSelectItem
devuelve false.
Menús de celda
Cada celda de un UICollectionView
es capaz de mostrar un menú que permite cortar, copiar y pegar en opcionalmente. Para crear un menú de edición en una celda:
- Invalide
ShouldShowMenu
y devuelva true si el elemento debe mostrar un menú. - Invalide
CanPerformAction
y devuelva true para cada acción que el elemento pueda realizar, que será cualquiera de cortar, copiar o pegar. - Invalide
PerformAction
para realizar la operación de edición y copia de pegado.
En la captura de pantalla siguiente se muestra el menú cuando se presiona una celda durante mucho tiempo:
Diseño
UICollectionView
admite un sistema de diseño que permite el posicionamiento de todos sus elementos, Celdas, Vistas complementarias y Vistas de decoración, para administrarse independientemente del propio UICollectionView
.
Con el sistema de diseño, una aplicación puede admitir diseños como el de cuadrícula que hemos visto en este artículo, así como proporcionar diseños personalizados.
Conceptos básicos de diseño
Los diseños de un UICollectionView
objeto se definen en una clase que hereda de UICollectionViewLayout
. La implementación de diseño es responsable de crear los atributos de diseño para cada elemento de UICollectionView
. Hay dos formas de crear un objeto:
- Use la instancia integrada de
UICollectionViewFlowLayout
. - Proporcione un diseño personalizado heredando de
UICollectionViewLayout
.
Diseño de flujo
La UICollectionViewFlowLayout
clase proporciona un diseño basado en línea que es adecuado para organizar el contenido en una cuadrícula de Celdas como hemos visto.
Para usar un diseño de flujo:
- Crea una instancia de
UICollectionViewFlowLayout
:
var layout = new UICollectionViewFlowLayout ();
- Pase la instancia al constructor de
UICollectionView
:
simpleCollectionViewController = new SimpleCollectionViewController (layout);
Esto es todo lo que se necesita para diseñar contenido en una cuadrícula. Además, cuando cambia la orientación, controla UICollectionViewFlowLayout
la reorganización adecuada del contenido, como se muestra a continuación:
Conjunto de secciones
Para proporcionar espacio alrededor de UIContentView
, los diseños tienen una SectionInset
propiedad de tipo UIEdgeInsets
. Por ejemplo, el código siguiente proporciona un búfer de 50 píxeles alrededor de cada sección de UIContentView
cuando se establece en UICollectionViewFlowLayout
:
var layout = new UICollectionViewFlowLayout ();
layout.SectionInset = new UIEdgeInsets (50,50,50,50);
Esto da como resultado el espaciado alrededor de la sección, como se muestra a continuación:
Subclase UICollectionViewFlowLayout
En edición para usar UICollectionViewFlowLayout
directamente, también se puede subclasar para personalizar aún más el diseño del contenido a lo largo de una línea. Por ejemplo, esto se puede usar para crear un diseño que no encapsula las celdas en una cuadrícula, sino que crea una sola fila con un efecto de desplazamiento horizontal, como se muestra a continuación:
Para implementar esto mediante subclases UICollectionViewFlowLayout
es necesario:
- Inicializar cualquier propiedad de diseño que se aplique al propio diseño o a todos los elementos del diseño en el constructor.
- Invalidando
ShouldInvalidateLayoutForBoundsChange
, devolviendo true para que, cuando se produzcan los límites de losUICollectionView
cambios, se volverá a calcular el diseño de las celdas. Esto se usa en este caso para asegurarse de que el código de transformación aplicado a la celda más central se aplicará durante el desplazamiento. - Invalidar
TargetContentOffset
para que la celda más central se ajuste al centro de comoUICollectionView
se detiene el desplazamiento. - Invalidación
LayoutAttributesForElementsInRect
para devolver una matriz deUICollectionViewLayoutAttributes
. CadaUICollectionViewLayoutAttribute
contiene información sobre cómo diseñar el elemento en particular, incluidas propiedades como susCenter
,Size
,ZIndex
yTransform3D
.
El siguiente código muestra tal implementación:
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);
}
}
}
Diseño personalizado
Además de usar UICollectionViewFlowLayout
, los diseños también se pueden personalizar completamente heredando directamente de UICollectionViewLayout
.
Los métodos clave que se van a invalidar son:
PrepareLayout
– Se usa para realizar cálculos geométricos iniciales que se usarán en todo el proceso de diseño.CollectionViewContentSize
: devuelve el tamaño del área utilizada para mostrar el contenido.LayoutAttributesForElementsInRect
– Al igual que con el ejemplo UICollectionViewFlowLayout mostrado anteriormente, este método se usa para proporcionar información alUICollectionView
respecto a cómo diseñar cada elemento. Sin embargo, a diferencia deUICollectionViewFlowLayout
, al crear un diseño personalizado, puede colocar elementos sin embargo.
Por ejemplo, el mismo contenido podría presentarse en un diseño circular, como se muestra a continuación:
Lo eficaz de los diseños es que cambiar del diseño similar a la cuadrícula, a un diseño de desplazamiento horizontal y, posteriormente, a este diseño circular solo requiere que se cambie la clase de diseño proporcionada UICollectionView
. Nada en UICollectionView
, su delegado o código fuente de datos cambia en absoluto.
Cambios de web en iOS 9
En iOS 9, la vista de colección (UICollectionView
) ahora admite la reordenación de elementos de serie mediante la adición de un nuevo reconocedor de gestos predeterminado y varios métodos auxiliares.
Con estos nuevos métodos, puede implementar fácilmente la acción de arrastrar para reordenar en la vista de colección y tiene la opción de personalizar la apariencia de los elementos durante cualquier fase del proceso de reordenación.
En este artículo, veremos cómo implementar arrastrar a reordenar en una aplicación de Xamarin.iOS, así como algunos de los otros cambios que iOS 9 ha realizado en el control de vista de colección:
Reordenación de elementos
Como se indicó anteriormente, uno de los cambios más significativos en la vista de colección en iOS 9 era la adición de la funcionalidad fácil de arrastrar a reordenar fuera de la caja.
En iOS 9, la forma más rápida de agregar reordenamiento a una vista de colección es usar un UICollectionViewController
.
El controlador de vista de colección ahora tiene una InstallsStandardGestureForInteractiveMovement
propiedad, que agrega un reconocedor de gestos estándar que admite arrastrar para reordenar los elementos de la colección.
Dado que el valor predeterminado es true
, solo tiene que implementar el MoveItem
método de la clase UICollectionViewDataSource
para admitir arrastrar a reordenar. Por ejemplo:
public override void MoveItem (UICollectionView collectionView, NSIndexPath sourceIndexPath, NSIndexPath destinationIndexPath)
{
// Reorder our list of items
...
}
Ejemplo de reordenación simple
Como ejemplo rápido, inicie un nuevo proyecto de Xamarin.iOS y edite el archivo Main.storyboard. Arrástre UICollectionViewController
a la superficie de diseño:
Seleccione la Vista de colección (puede ser más fácil hacerlo desde el esquema del documento). En la pestaña diseño del Panel de propiedades, establezca los siguientes tamaños, como se muestra en la captura de pantalla siguiente:
- Tamañode celda: Ancho – 60 | Alto : 60
- Tamaño delencabezado: Ancho – 0 | Alto : 0
- Tamañodel pie de página: Ancho – 0 | Alto : 0
- Espaciadomínimo: para celdas – 8 | Para líneas – 8
- Conjuntos de secciones: Top – 16 | Inferior – 16 | Izquierda – 16 | Derecha – 16
A continuación, edite la celda predeterminada:
- Cambiar su color de fondo a azul
- Agregar una etiqueta para que actúe como título de la celda
- Establecimiento del identificador de reutilización en la celda
Agregue restricciones para mantener la etiqueta centrada dentro de la celda a medida que cambia el tamaño:
En el Panel de propiedades de CollectionViewCell y establezca la clase TextCollectionViewCell
en :
Establezca la vista reutilizable colección en Cell
:
Por último, seleccione la etiqueta y asígnela TextLabel
el nombre :
Edite la TextCollectionViewCell
clase y agregue las siguientes propiedades:
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
}
}
Aquí la Text
propiedad de la etiqueta se expone como título de la celda, por lo que se puede establecer desde el código.
Agregue una nueva clase de C# al proyecto y llámela WaterfallCollectionSource
. A continuación, edite el archivo y haga que tenga un aspecto similar al siguiente:
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
}
}
Esta clase será el origen de datos de nuestra vista de recopilación y proporcionará la información de cada celda de la colección.
Observe que el MoveItem
método se implementa para permitir que los elementos de la colección se arrastren de nuevo.
Agregue otra nueva clase de C# al proyecto y llámela WaterfallCollectionDelegate
. Edite este archivo y haga que tenga un aspecto similar al siguiente:
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
}
}
Esto actuará como delegado para nuestra vista de colección. Los métodos se han invalidado para resaltar una celda a medida que el usuario interactúa con ella en la vista de colección.
Agregue una última clase de C# al proyecto y llámala WaterfallCollectionView
. Edite este archivo y haga que tenga un aspecto similar al siguiente:
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
}
}
Observe que DataSource
y Delegate
que hemos creado anteriormente se establecen cuando la vista de colección se construye a partir de su guión gráfico (o archivo .xib).
Vuelva a editar el archivo Main.storyboard y seleccione la vista de colección y cambie a propiedades . Establezca la clase en la clase personalizada WaterfallCollectionView
que definimos anteriormente:
Guarde los cambios realizados en la interfaz de usuario y ejecute la aplicación. Si el usuario selecciona un elemento de la lista y lo arrastra a una nueva ubicación, los demás elementos se animarán automáticamente a medida que salen del camino del elemento. Cuando el usuario quita el elemento en una nueva ubicación, se pegará a esa ubicación. Por ejemplo:
Usar un reconocedor de gestos personalizado
En los casos en los que no puede usar y debe usar UICollectionViewController
un elemento normal UIViewController
, o si desea tomar más control sobre el gesto de arrastrar y colocar, puede crear su propio reconocedor de gestos personalizado y agregarlo a la vista de colección cuando se cargue la vista. Por ejemplo:
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);
}
Aquí se usan varios métodos nuevos agregados a la vista de recopilación para implementar y controlar la operación de arrastre:
BeginInteractiveMovementForItem
- Marca el inicio de una operación de movimiento.UpdateInteractiveMovementTargetPosition
- Se envía a medida que se actualiza la ubicación del elemento.EndInteractiveMovement
- Marca el final de un movimiento de elemento.CancelInteractiveMovement
- Marca al usuario que cancela la operación de movimiento.
Cuando se ejecuta la aplicación, la operación de arrastre funcionará exactamente igual que el reconocedor de gestos de arrastre predeterminado que viene con la vista de colección.
Diseños personalizados y reordenación
En iOS 9, se han agregado varios métodos nuevos para trabajar con diseños personalizados y de arrastrar a reordenar en una vista de colección. Para explorar esta característica, vamos a agregar un diseño personalizado a la colección.
En primer lugar, añada una nueva clase C# llamada WaterfallCollectionLayout
al proyecto. Edítelo y fíjelo como el siguiente:
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
}
}
Se puede usar la clase para proporcionar un diseño de tipo de cascada personalizado de dos columnas a la vista de colección.
El código usa la codificación clave-valor (a través de los WillChangeValue
métodos y DidChangeValue
) para proporcionar enlace de datos para nuestras propiedades calculadas en esta clase.
A continuación, edite y WaterfallCollectionSource
realice los siguientes cambios y adiciones:
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);
}
}
Esto creará un alto aleatorio para cada uno de los elementos que se mostrarán en la lista.
A continuación, edite la WaterfallCollectionView
clase y agregue la siguiente propiedad auxiliar:
public WaterfallCollectionSource Source {
get { return (WaterfallCollectionSource)DataSource; }
}
Esto hará que sea más fácil obtener en nuestro origen de datos (y los alto del elemento) desde el diseño personalizado.
Por último, edite el controlador de vista y agregue el código siguiente:
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);
}
Esto crea una instancia de nuestro diseño personalizado, establece el evento para proporcionar el tamaño de cada elemento y adjunta el nuevo diseño a nuestra vista de colección.
Si se vuelve a ejecutar la aplicación de Xamarin.iOS, la vista de recopilación será similar a la siguiente:
Todavía podemos arrastrar a reordenar elementos como antes, pero los elementos ahora cambiarán el tamaño para ajustarse a su nueva ubicación cuando se quiten.
Cambios en la vista de colección
En las secciones siguientes, echaremos un vistazo detallado a los cambios realizados en cada clase de la vista de colección de iOS 9.
UICollectionView
Se han realizado los siguientes cambios o adiciones a la UICollectionView
clase para iOS 9:
BeginInteractiveMovementForItem
: marca el inicio de una operación de arrastre.CancelInteractiveMovement
: informa a la vista de recopilación de que el usuario ha cancelado una operación de arrastre.EndInteractiveMovement
: informa a la vista de colección de que el usuario ha finalizado una operación de arrastre.GetIndexPathsForVisibleSupplementaryElements
: devuelve elindexPath
de un encabezado o pie de página en una sección de vista de colección.GetSupplementaryView
: devuelve el encabezado o pie de página especificados.GetVisibleSupplementaryViews
: devuelve una lista de todos los encabezados y pies de página visibles.UpdateInteractiveMovementTargetPosition
– Informa a la vista de colección que el usuario ha movido o está moviendo un elemento durante una operación de arrastre.
UICollectionViewController
Se han realizado los siguientes cambios o adiciones a la UICollectionViewController
clase en iOS 9:
InstallsStandardGestureForInteractiveMovement
– Sitrue
se usará el nuevo reconocedor de gestos que admite automáticamente arrastrar a reordenar.CanMoveItem
: informa a la vista de colección si se puede arrastrar un elemento determinado de nuevo.GetTargetContentOffset
: se usa para obtener el desplazamiento de un elemento de vista de colección determinado.GetTargetIndexPathForMove
: obtiene elindexPath
de un elemento determinado para una operación de arrastre.MoveItem
: mueve el orden de un elemento determinado en la lista.
UICollectionViewDataSource
Se han realizado los siguientes cambios o adiciones a la UICollectionViewDataSource
clase en iOS 9:
CanMoveItem
: informa a la vista de colección si se puede arrastrar un elemento determinado de nuevo.MoveItem
: mueve el orden de un elemento determinado en la lista.
UICollectionViewDelegate
Se han realizado los siguientes cambios o adiciones a la UICollectionViewDelegate
clase en iOS 9:
GetTargetContentOffset
: se usa para obtener el desplazamiento de un elemento de vista de colección determinado.GetTargetIndexPathForMove
: obtiene elindexPath
de un elemento determinado para una operación de arrastre.
UICollectionViewFlowLayout
Se han realizado los siguientes cambios o adiciones a la UICollectionViewFlowLayout
clase en iOS 9:
SectionFootersPinToVisibleBounds
: pega los pies de página de sección a los límites de la vista de colección visibles.SectionHeadersPinToVisibleBounds
: se pegan los encabezados de sección a los límites de la vista de colección visibles.
UICollectionViewLayout
Se han realizado los siguientes cambios o adiciones a la UICollectionViewLayout
clase en iOS 9:
GetInvalidationContextForEndingInteractiveMovementOfItems
: devuelve el contexto de invalidación al final de una operación de arrastre cuando el usuario finaliza el arrastre o lo cancela.GetInvalidationContextForInteractivelyMovingItems
: devuelve el contexto de invalidación al principio de una operación de arrastre.GetLayoutAttributesForInteractivelyMovingItem
: obtiene los atributos de diseño de un elemento determinado mientras arrastra un elemento.GetTargetIndexPathForInteractivelyMovingItem
: devuelve elindexPath
del elemento que se encuentra en el punto especificado al arrastrar un elemento.
UICollectionViewLayoutAttributes
Se han realizado los siguientes cambios o adiciones a la UICollectionViewLayoutAttributes
clase en iOS 9:
CollisionBoundingPath
: devuelve la ruta de colisión de dos elementos durante una operación de arrastre.CollisionBoundsType
: devuelve el tipo de colisión (como )UIDynamicItemCollisionBoundsType
que se ha producido durante una operación de arrastre.
UICollectionViewLayoutInvalidationContext
Se han realizado los siguientes cambios o adiciones a la UICollectionViewLayoutInvalidationContext
clase en iOS 9:
InteractiveMovementTarget
: devuelve el elemento de destino de una operación de arrastre.PreviousIndexPathsForInteractivelyMovingItems
: devuelve elindexPaths
de otros elementos implicados en una operación de reordenación.TargetIndexPathsForInteractivelyMovingItems
: devuelve losindexPaths
elementos que se reordenarán como resultado de una operación de arrastrar a reordenar.
UICollectionViewSource
Se han realizado los siguientes cambios o adiciones a la UICollectionViewSource
clase en iOS 9:
CanMoveItem
: informa a la vista de colección si se puede arrastrar un elemento determinado de nuevo.GetTargetContentOffset
: devuelve los desplazamientos de los elementos que se moverán a través de una operación de arrastrar a reordenar.GetTargetIndexPathForMove
: devuelve elindexPath
de un elemento que se moverá durante una operación de arrastrar a reordenar.MoveItem
: mueve el orden de un elemento determinado en la lista.
Resumen
En este artículo se han tratado los cambios en las vistas de colección de iOS 9 y se ha descrito cómo implementarlos en Xamarin.iOS. Se ha tratado la implementación de una acción simple de arrastrar a reordenar en una vista de colección; usar un reconocedor de gestos personalizado con arrastrar a reordenar; y cómo el arrastrar a reordenar afecta a un diseño de vista de colección personalizado.