UICollectionViewLayout para pizza de diferentes mitades

Para hacer pizza a partir de mitades, utilizamos dos UICollectionViewLayout . Estoy hablando de cómo escribimos ese diseño para iOS, lo que encontramos y lo que rechazamos.



Prototipo


Cuando tuvimos la tarea de hacer una interfaz para pizza a partir de mitades, estábamos un poco confundidos. Lo quiero maravillosamente, y claramente, y convenientemente, y grande, e interactivamente y mucho más. Quiero hacerlo bien


Los diseñadores probaron diferentes enfoques: una cuadrícula de pizzas, tarjetas horizontales y verticales, pero se asentaron en el medio golpe. No sabíamos cómo lograr ese resultado, por lo que comenzamos con un experimento y el prototipo tardó dos semanas. Incluso el diseño burdo fue capaz de complacer a todos. La reacción se grabó en video:



Cómo funciona UICollectionView


UICollectionView es una subclase de UIScrollView , y es una UIView normal, con bounds cambian de un deslizamiento. Moviéndolo .origin , cambiamos el área visible, y cambiar .size afecta la escala.


Cuando la pantalla se UICollectionView crea (o reutiliza) las celdas, y las reglas para mostrarlas se describen en UICollectionViewLayout . Trabajaremos con él.


Las posibilidades de UICollectionViewLayout grandes, puede especificar cualquier relación entre celdas. Por ejemplo, puede hacer algo muy similar a lo que puede hacer iCarousel :



Primer acercamiento


Cambiar el aspecto al mover la pantalla me facilitó la comprensión del diseño del diseño.
Estamos acostumbrados al hecho de que las celdas se mueven alrededor de la pantalla (el rectángulo verde es la pantalla del teléfono):



Pero viceversa, esta pantalla se mueve en relación con las celdas. Los árboles están quietos, este tren viaja:



En el ejemplo, los marcos de las celdas no cambian, pero los bounds la colección sí cambian. Origin estos bounds es el contentOffset que conocemos.


Para crear un diseño, debe pasar por dos etapas:


  • calcular los tamaños de todas las celdas
  • mostrar solo visible en la pantalla.

Diseño simple como en UITableView


El diseño no funciona con celdas directamente. En cambio, usan UICollectionViewLayoutAttributes : este es el conjunto de parámetros que se aplicarán a la celda. Frame : el principal, es responsable de la posición y el tamaño de la celda. Otros parámetros: transparencia, desplazamiento, posición en la profundidad de la pantalla, etc.



Para comenzar, escribamos un UICollectionViewLayout simple, que repite el comportamiento de un UITableView : las celdas ocupan todo el ancho, van una tras otra.


4 pasos por delante:


  • Calcule el frame para todas las celdas en el método de prepare .
  • Devuelve celdas visibles en layoutAttributesForElements(in:) método.
  • Devuelva los parámetros de celda por su índice en el layoutAttributesForItem(at:) . Por ejemplo, este método se usa cuando se llama al método de colección scrollToItem (en :).
  • Devuelve las dimensiones del contenido resultante en collectionViewContentSize . Entonces, el recolector descubrirá a dónde se puede desplazar el borde.

En la mayoría de los dispositivos, el tamaño de la pizza será de 300 puntos, luego las coordenadas y tamaños de todas las celdas:



Hice los cálculos en una clase separada. Consta de dos partes: calcula todos los marcos en el constructor y luego solo da acceso a los resultados finales:


 class TableLayoutCache { // MARK: - Calculation func recalculateDefaultFrames(numberOfItems: Int) { defaultFrames = (0..<numberOfItems).map { defaultCellFrame(atRow: $0) } } func defaultCellFrame(atRow row: Int) -> CGRect { let y = itemSize.height * CGFloat(row) let defaultFrame = CGRect(x: 0, y: y, width: collectionWidth, height: itemSize.height) return defaultFrame } // MARK: - Access func visibleRows(in frame: CGRect) -> [Int] { return defaultFrames .enumerated() // Index to frame relation .filter { $0.element.intersects(frame)} // Filter by frame .map { $0.offset } // Return indexes } var contentSize: CGSize { return CGSize(width: collectionWidth, height: defaultFrames.last?.maxY ?? 0) } static var zero: TableLayoutCache { return TableLayoutCache(itemSize: .zero, collectionWidth: 0) } init(itemSize: CGSize, collectionWidth: CGFloat) { self.itemSize = itemSize self.collectionWidth = collectionWidth } private let itemSize: CGSize private let collectionWidth: CGFloat private var defaultFrames = [CGRect]() } 

Luego, en la clase de diseño, solo necesita pasar los parámetros del caché.


  1. El método de prepare invoca el cálculo de todos los marcos.
  2. layoutAttributesForElements (en :) filtrará los marcos. Si el marco se cruza con el área visible, entonces la celda debe mostrarse: calcule todos los atributos y devuélvala a la matriz de celdas visibles.
  3. layoutAttributesForItem (at :) - Calcula los atributos para una sola celda.

 class TableLayout: UICollectionViewLayout { override var collectionViewContentSize: CGSize { return cache.contentSize } override func prepare() { super.prepare() let numberOfItems = collectionView!.numberOfItems(inSection: section) cache = TableLayoutCache(itemSize: itemSize, collectionWidth: collectionView!.bounds.width) cache.recalculateDefaultFrames(numberOfItems: numberOfItems) } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let indexes = cache.visibleRows(in: rect) let cells = indexes.map { (row) -> UICollectionViewLayoutAttributes? in let path = IndexPath(row: row, section: section) let attributes = layoutAttributesForItem(at: path) return attributes }.compactMap { $0 } return cells } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) attributes.frame = cache.defaultCellFrame(atRow: indexPath.row) return attributes } var itemSize: CGSize = .zero { didSet { invalidateLayout() } } private let section = 0 var cache = TableLayoutCache.zero } 

Cambiamos de acuerdo a sus necesidades.


Hemos ordenado la vista de tabla, pero ahora necesitamos hacer un diseño dinámico. En cada cambio de dedo, volveremos a calcular los atributos de las celdas: tome fotogramas que ya se han contado y cámbielos usando .transform . Todos los cambios se realizarán en una subclase de PizzaHalfSelectorLayout .


Leemos el índice actual de pizza


Para mayor comodidad, puede olvidarse de contentOffset y reemplazarlo con el número de la pizza actual. Entonces ya no necesitará pensar en las coordenadas, todas las decisiones serán en torno al número de pizza y su grado de desplazamiento desde el centro de la pantalla.


Se necesitan dos métodos: uno convierte contentOffset en un número de pizza, el otro viceversa:


 extension PizzaHalfSelectorLayout { func contentOffset(for pizzaIndex: Int) -> CGPoint { let cellHeight = itemSize.height let screenHalf = collectionView!.bounds.height / 2 let midY = cellHeight * CGFloat(pizzaIndex) + cellHeight / 2 let newY = midY - screenHalf return CGPoint(x: 0, y: newY) } func pizzaIndex(offset: CGPoint) -> CGFloat { let cellHeight = itemSize.height let proposedCenterY = collectionView!.screenCenterYOffset(for: offset) let pizzaIndex = proposedCenterY / cellHeight return pizzaIndex } } 

El cálculo de contentOffset para el centro de la pantalla se representa en extension :


 extension UIScrollView { func screenCenterYOffset(for offset: CGPoint? = nil) -> CGFloat { let offsetY = offset?.y ?? contentOffset.y let contentOffsetY = offsetY + bounds.height / 2 return contentOffsetY } } 

Nos detenemos en la pizza del centro.


Lo primero que debemos hacer es detener la pizza en el centro de la pantalla. El targetContentOffset(forProposedContentOffset:) pregunta dónde detenerse si a la velocidad actual se detendría en proposedContentOffset .


El cálculo es simple: mira en qué pizza caerá el contenido del contenido proposedContentOffset y desplázate para que quede en el centro:


  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { let pizzaIndex = Int(self.pizzaIndex(offset: proposedContentOffset)) let projectedOffset = contentOffset(for: pizzaIndex) return projectedOffset } 

UIScrollView tiene dos .normal desplazamiento: .normal y .fast . .fast más adecuado para .fast :


 collectionView!.decelerationRate = .fast 

Pero hay un problema: si nos desplazamos solo un poco, entonces debemos quedarnos en la pizza y no pasar al siguiente. No hay ningún método para cambiar la velocidad, por lo que el rebote inverso, aunque a poca distancia, pero a una velocidad muy alta:



¡Atención, pirateo!


Si la celda no cambia, devolvemos el contentOffset actual, por lo que el desplazamiento se detiene. Luego, nosotros mismos nos desplazaremos al lugar anterior usando el scrollToItem estándar. Por desgracia, también tiene que desplazarse de forma asincrónica, de modo que el código se llame después del return , luego habrá poca atenuación durante la animación.


  override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { let pizzaIndex = Int(self.pizzaIndex(offset: proposedContentOffset)) let projectedOffset = contentOffset(for: pizzaIndex) let sameCell = pizzaIndex == currentPizzaIndexInt if sameCell { animateBackwardScroll(to: pizzaIndex) return collectionView!.contentOffset // Stop scroll, we've animated manually } return projectedOffset } /// A bit of magic. Without that, high velocity moves cells backward very fast. /// We slow down the animation private func animateBackwardScroll(to pizzaIndex: Int) { let path = IndexPath(row: pizzaIndex, section: 0) collectionView?.scrollToItem(at: path, at: .centeredVertically, animated: true) // More magic here. Fix double-step animation. // You can't do it smoothly without that. DispatchQueue.main.async { self.collectionView?.scrollToItem(at: path, at: .centeredVertically, animated: true) } } 

El problema se ha ido, ahora la pizza regresa sin problemas:



Aumentar la pizza central


Contamos el diseño al mover


Es necesario hacer que la pizza central aumente gradualmente a medida que se acerca al centro. Para hacer esto, necesita calcular los parámetros no una vez al inicio, sino cada vez en el desplazamiento. Se enciende simplemente:


  override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true } 

Ahora, con cada desplazamiento, se layoutAttributesForElements(in:) métodos prepare y layoutAttributesForElements(in:) . Por lo tanto, podemos actualizar UICollectionViewLayoutAttributes muchas veces seguidas, cambiando suavemente la posición y la transparencia.


Transformar células


En el diseño de la tabla, las celdas estaban una debajo de la otra y sus coordenadas se contaron una vez. Ahora los cambiaremos dependiendo de la posición relativa al centro de la pantalla. Agregue un método que los cambie sobre la marcha.


En el método layoutAttributesForElements , debe obtener los atributos de la superclase, filtrar los atributos de las celdas y pasarlos al método updateCells :


  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let elements = super.layoutAttributesForElements(in: rect) else { return nil } let cells = elements.filter { $0.representedElementCategory == .cell } self.updateCells(cells) } 

Ahora cambiaremos los atributos de la celda en una función:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) 

Durante el movimiento, necesitamos cambiar la transparencia, el tamaño y mantener la pizza en el centro.


La posición de la celda con respecto al centro de la pantalla se presenta convenientemente en una forma normalizada. Si la celda está en el centro, entonces el parámetro es 0, si se desplaza, entonces el parámetro cambia de -1 cuando se mueve hacia arriba, a 1 cuando se mueve. Si los valores están más lejos de cero que 1 / -1, entonces esto significa que la celda ya no es central y ha dejado de cambiar. Llamé a este parámetro de escala:



Necesita calcular la diferencia entre el centro del marco y el centro de la pantalla. Dividiendo la diferencia por una constante, normalizamos el valor, y min y max conducirán a un rango de -1 a +1:


 extension PizzaHalfSelectorLayout { func scale(for row: Int) -> CGFloat { let frame = cache.defaultCellFrame(atRow: row) let scale = self.scale(for: frame) return scale.normalized } func scale(for frame: CGRect) -> CGFloat { let criticalOffset = PizzaHalfSelectorLayout.criticalOffsetFromCenter // 200 pt let centerOffset = offsetFromScreenCenter(frame) let relativeOffset = centerOffset / criticalOffset return relativeOffset } func offsetFromScreenCenter(_ frame: CGRect) -> CGFloat { return frame.midY - collectionView!.screenCenterYOffset() } } extension CGFloat { var normalized: CGFloat { return CGFloat.minimum(1, CGFloat.maximum(-1, self)) } } 

Tamaño


Tener una scale normalizada, puede hacer cualquier cosa. Los cambios de -1 a +1 son demasiado fuertes, necesitan ser convertidos por tamaño. Por ejemplo, queremos que el tamaño disminuya a un máximo de 0.6 del tamaño de la pizza central:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) { for cell in cells { let normScale = scale(for: cell.indexPath.row) let scale = 1 - PizzaHalfSelectorLayout.scaleFactor * abs(normScale) cell.transform = CGAffineTransform(scaleX: scale, y: scale) } } 

.transform cambia el tamaño en relación con el centro de las celdas. La celda central tiene normScale = 0, su tamaño no cambia:



Transparencia


La transparencia se puede cambiar a través del parámetro alpha . El valor de scale que usamos en la transform también es adecuado.


  cell.alpha = scale 

Ahora la pizza cambia su tamaño y transparencia. Ya no es tan aburrido como en las mesas normales.



Bisectando


Antes de eso, trabajamos con una pizza: establecimos el sistema de referencia desde el centro, cambiamos el tamaño y la transparencia. Ahora necesitas dividir por la mitad.


Usar una colección para esto es demasiado difícil: necesitará escribir su propio controlador de gestos para cada mitad. Es más fácil hacer dos colecciones, cada una con su propio diseño. Solo que ahora, en lugar de una pizza entera, habrá mitades.


Dos controladores, un contenedor


Casi siempre, UIViewController una pantalla en varios UIViewController , cada uno con su propia tarea. Esta vez resultó así:



  1. El controlador principal: todas las partes están ensambladas en él y el botón "mezclar".
  2. Controlador con dos contenedores para mitades, una firma central e indicadores de desplazamiento.
  3. El controlador con un colector (blanco derecho).
  4. Panel inferior con precio.

Para distinguir entre la mitad izquierda y la derecha, comencé enum , se almacena en el diseño en la .orientation .orientation:


 enum PizzaHalfOrientation { case left case right func opposite() -> PizzaHalfOrientation { switch self { case .left: return .right case .right: return .left } } } 

Desplazamos las mitades al centro


El diseño anterior dejó de hacer lo que esperamos: las mitades comenzaron a desplazarse al centro de sus colecciones, no al centro de la pantalla:



La solución es simple: debe desplazar horizontalmente las celdas hasta la mitad del centro de la pantalla:


  func centerAlignedFrame(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGRect { let hOffset = horizontalOffset(for: element, scale: scale) switch orientation { case .left: // Align to right return element.frame.offsetBy(dx: +hOffset - spaceBetweenHalves / 2, dy: 0) case .right: // Align to left return element.frame.offsetBy(dx: -hOffset + spaceBetweenHalves / 2, dy: 0) } } private func horizontalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let collectionWidth = collectionView!.bounds.width let scaledElementWidth = element.frame.width * scale let hOffset = (collectionWidth - scaledElementWidth) / 2 return hOffset } 

La distancia entre las mitades se controla de inmediato.



Desplazamiento de celda


Era fácil colocar la pizza redonda en un cuadrado, y para la mitad se necesita medio cuadrado:



Puede reescribir el cálculo de cuadros: reducir a la mitad el ancho, alinear los cuadros con el centro de manera diferente para cada mitad. Para simplificar, simplemente cambie el contentMode imagen dentro de la celda:


 class PizzaHalfCell: UICollectionViewCell { var halfOrientation: PizzaHalfOrientation = .left { didSet { imageView?.contentMode = halfOrientation == .left ? .topRight : .topLeft self.setNeedsLayout() } } } 


Presione la pizza verticalmente


Las pizzas disminuyeron, pero la distancia entre sus centros no cambió, aparecieron grandes brechas. Puede compensarlos de la misma manera que alineamos las mitades en el centro.


  private func verticalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let offsetFromCenter = offsetFromScreenCenter(element.frame) let vOffset: CGFloat = PizzaHalfSelectorLayout.verticalOffset( offsetFromCenter: offsetFromCenter, scale: scale) return vOffset } static func verticalOffset(offsetFromCenter: CGFloat, scale: CGFloat) -> CGFloat { return -offsetFromCenter / 4 * scale } 

Como resultado, todas las compensaciones se ven así:


  func centerAlignedFrame(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGRect { let hOffset = horizontalOffset(for: element, scale: scale) let vOffset = verticalOffset (for: element, scale: scale) switch orientation { case .left: // Align to right return element.frame.offsetBy(dx: +hOffset - spaceBetweenHalves / 2, dy: vOffset) case .right: // Align to left return element.frame.offsetBy(dx: -hOffset + spaceBetweenHalves / 2, dy: vOffset) } } 

Y la configuración de la celda es así:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) { for cell in cells { let normScale = scale(for: cell.indexPath.row) let scale = 1 - PizzaHalfSelectorLayout.scaleFactor * abs(normScale) cell.alpha = scale cell.frame = centerAlignedFrame(for: cell, scale: scale) cell.transform = CGAffineTransform(scaleX: scale, y: scale) cell.zIndex = cellZLevel } } 

No confunda: la configuración del marco debe ser anterior a la transformación. Si cambia el orden, el resultado de los cálculos será completamente diferente.


Hecho Cortamos las mitades y las alineamos al centro:



Añadir subtítulos


Los encabezados se crean de la misma manera que las celdas, solo que en lugar de UICollectionViewLayoutAttributes(forCellWith:) , debe usar el constructor UICollectionViewLayoutAttributes(forSupplementaryViewOfKind:)
y devolverlos junto con los parámetros de celda en layoutAttributesForElements(in:)


Primero, describimos un método para obtener un encabezado por IndexPath :


  override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { let attributes = UICollectionViewLayoutAttributes( forSupplementaryViewOfKind: elementKind, with: indexPath) attributes.frame = defaultFrameForHeader(at: indexPath) attributes.zIndex = headerZLevel return attributes } 

El cálculo del marco está oculto en el método defaultFrameForHeader (más adelante).


Ahora puede obtener el IndexPath celdas visibles y mostrar las IndexPath para ellas:


  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { … let visiblePaths = cells.map { $0.indexPath } let headers = self.headers(for: visiblePaths) updateHeaders(headers) return cells + headers } 

Una llamada de función terriblemente larga está oculta en los headers(for:) método headers(for:) :


  func headers(for paths: [IndexPath]) -> [UICollectionViewLayoutAttributes] { let headers: [UICollectionViewLayoutAttributes] = paths.map { layoutAttributesForSupplementaryView( ofKind: UICollectionView.elementKindSectionHeader, at: $0) }.compactMap { $0 } return headers } 

zIndex


Ahora las celdas y las firmas están al mismo nivel de "altura", por lo que pueden superponerse entre sí. Para mantener los encabezados siempre más altos, dele zIndex mayor que cero. Por ejemplo, 100.


Arreglamos una posición (en realidad no)


Las firmas fijadas en la pantalla son un poco desconcertantes. Quiere arreglar, pero viceversa, moverse constantemente con bounds :



Todo es simple en el código: obtenemos la posición de la firma en la pantalla y la contentOffset a contentOffset :


  func defaultFrameForHeader(at indexPath: IndexPath) -> CGRect { let inset = max(collectionView!.layoutMargins.left, collectionView!.layoutMargins.right) let y = collectionView!.bounds.minY let height = collectionView!.bounds.height let width = collectionView!.bounds.width let headerWidth = width - inset * 2 let headerHeight: CGFloat = 60 let vOffset: CGFloat = 30 let screenY = (height - itemSize.height) / 2 - headerHeight / 2 - vOffset return CGRect(x: inset, y: y + screenY, width: headerWidth, height: headerHeight) } 

La altura de las firmas puede ser diferente, es mejor considerarla en el delegado (y en el caché allí).


Firmas animadas


Todo es muy similar a las células. Según la scale actual, puede calcular la transparencia de la celda. El desplazamiento se puede establecer a través de .transform , por lo que la inscripción se desplazará en relación con su marco:


  func updateHeaders(_ headers: [UICollectionViewLayoutAttributes]) { for header in headers { let scale = self.scale(for: header.indexPath.row) let alpha = 1 - abs(scale) header.alpha = alpha let translation = 20 * scale header.transform = CGAffineTransform(translationX: 0, y: translation) } } 


Optimizar


Después de agregar encabezados, el rendimiento bajó drásticamente. Sucedió porque UICollectionViewLayoutAttributes las firmas, pero aún las UICollectionViewLayoutAttributes a UICollectionViewLayoutAttributes . A partir de esto, los encabezados se agregan a la jerarquía, participan en el diseño, pero no se muestran. Mostramos solo las celdas que se cruzan con los bounds actuales, y los encabezados deben filtrarse por alpha :


  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { … let visibleHeaders = headers.filter { $0.alpha > 0 } return cells + visibleHeaders } 

Estamos de acuerdo con la firma central (receta original)


Hicimos un gran trabajo, pero había una contradicción en la interfaz: si elige dos mitades idénticas, se convierten en una pizza normal.


Decidimos dejarlo así, pero procesamos correctamente la condición, mostrando que era una pizza normal. Nuestra nueva tarea es mostrar una etiqueta en el centro para pizzas idénticas, y esconderse a lo largo de los bordes.



El diseño solo es demasiado difícil de resolver, porque la inscripción se encuentra en la unión de dos coleccionistas. Será más fácil si el controlador, que contiene ambas colecciones, coordina el movimiento de todas las firmas.


Al desplazarse, pasamos el índice actual al controlador, envía el índice a la mitad opuesta. Si los índices coinciden, entonces muestra el título de la pizza original, y si es diferente, entonces las firmas para cada mitad son visibles.


Cómo inventar tus diseños


La parte más difícil fue descubrir cómo poner su idea en la informática. Por ejemplo, quiero que las pizzas rueden como un tambor. Para entender el problema, pasé por 4 pasos habituales:


  1. Dibujé un par de estados.
  2. Comprendí cómo los elementos están relacionados con la posición de la pantalla (los elementos se mueven en relación con el centro de la pantalla).
  3. Variables creadas con las que es conveniente trabajar (centro de la pantalla, marco central de pizza, escala).
  4. Se me ocurrieron pasos simples, cada uno de los cuales se puede verificar.

Los estados y las animaciones son fáciles de dibujar en Keynote. Tomé el diseño estándar y dibujé los dos primeros pasos:



El video resulta así:



Tomó tres cambios:


  1. En lugar de marcos del caché, tomaremos centerPizzaFrame .
  2. Usando la scale lea el desplazamiento de este marco.
  3. Recalcular zIndex .


     func centerAlignedFrame(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGRect { let hOffset = self.horizontalOffset(for: element, scale: scale) let vOffset = self.verticalOffset (for: element, scale: scale) switch self.pizzaHalf { case .left: // Align to right return centerPizzaFrame.offsetBy(dx: hOffset - spaceBetweenHalves / 2, dy: vOffset) case .right: // Align to left return centerPizzaFrame.offsetBy(dx: -hOffset + spaceBetweenHalves / 2, dy: vOffset) } } private func horizontalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let collectionWidth = self.collectionView!.bounds.width let scaledElementWidth = centerPizzaFrame.width * scale let hOffset = (collectionWidth - scaledElementWidth) / 2 return hOffset } private func verticalOffset(for element: UICollectionViewLayoutAttributes, scale: CGFloat) -> CGFloat { let totalProgress = self.scale(for: element.frame).normalized(by: 1) let criticalOffset = PizzaHalfSelectorLayout.criticalOffsetFromCenter * 1.1 return totalProgress * criticalOffset } 


, zIndex . : , , zIndex .


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) { for cell in cells { let normScale = self.scale(for: cell.indexPath.row) let scale = 1 - PizzaHalfSelectorLayout.scaleFactor * abs(normScale) cell.alpha = 1//scale cell.frame = self.centerAlignedFrame(for: cell, scale: scale) cell.transform = CGAffineTransform(scaleX: scale, y: scale) cell.zIndex = self.zIndex(row: cell.indexPath.row) } } private func zIndex(row: Int) -> Int { let numberOfCells = self.cache.defaultFrames.count if row == self.currentPizzaIndexInt { return numberOfCells } else if row < self.currentPizzaIndexInt { return row } else { return numberOfCells - row - 1 } } 

, , :


 row: zIndex` 0: 0 1: 1 2: 2 3: 10 —   4: 5 5: 4 6: 3 7: 2 8: 1 9: 0 

, .



, : , . : , .


, :


  • ,
  • , : , ,
  • ,
  • ,
  • -,
  • «»,
  • Voice Over.

:



github , .


UICollectionViewLayout , A Tour of UICollectionView


, .

Source: https://habr.com/ru/post/452876/


All Articles