UICollectionViewLayout para pizza de diferentes partes

Para fazer pizza a partir da metade, usamos dois UICollectionViewLayout . Estou falando sobre como escrevemos esse layout para iOS, o que encontramos e recusamos.



Protótipo


Quando tivemos a tarefa de criar uma interface para pizza a partir de metades, ficamos um pouco confusos. Eu quero isso de forma bonita, clara e conveniente, e grande, interativa e muito mais. Eu quero ser legal.


Os designers tentaram abordagens diferentes: uma grade de pizzas, cartões horizontais e verticais, mas se estabeleceram no meio golpe. Não sabíamos como obter esse resultado. Começamos com um experimento e demoramos duas semanas para prototipar. Até o layout bruto foi capaz de agradar a todos. A reação foi gravada em vídeo:



Como o UICollectionView funciona


UICollectionView é uma subclasse de UIScrollView e é uma UIView regular, com bounds alterados de um furto. Movendo-o .origin , mudamos a área visível e a alteração do .size afeta a escala.


Quando a tela se UICollectionView cria (ou reutiliza) as células, e as regras para exibi-las são descritas em UICollectionViewLayout . Vamos trabalhar com ele.


As possibilidades de UICollectionViewLayout grandes, você pode especificar qualquer relacionamento entre células. Por exemplo, você pode fazer algo muito semelhante ao que o iCarousel pode fazer :



Primeira abordagem


Alterar a aparência da movimentação da tela facilitou a compreensão do layout do layout.
Estamos acostumados ao fato de que as células se movem pela tela (o retângulo verde é a tela do telefone):



Mas vice-versa, essa tela se move em relação às células. As árvores estão paradas, este trem viaja:



No exemplo, os quadros da célula não são alterados, mas os bounds própria coleção são alterados. Origin desses bounds é o contentOffset conhecido por nós.


Para criar um layout, você precisa passar por dois estágios:


  • calcular os tamanhos de todas as células
  • mostrar apenas visível na tela.

Layout simples como no UITableView


O layout não funciona diretamente com células. Em vez disso, eles usam UICollectionViewLayoutAttributes - este é o conjunto de parâmetros que serão aplicados à célula. Frame - o principal, é responsável pela posição e tamanho da célula. Outros parâmetros: transparência, deslocamento, posição na profundidade da tela, etc.



Para começar, UICollectionViewLayout escrever um UICollectionViewLayout simples, que repete o comportamento de um UITableView : as células ocupam toda a largura, siga uma após a outra.


4 passos à frente:


  • Calcular o frame para todas as células no método de prepare .
  • Retornar células visíveis no layoutAttributesForElements(in:) .
  • Retorne os parâmetros da célula por seu índice no método layoutAttributesForItem(at:) . Por exemplo, esse método é usado ao chamar o método de coleção scrollToItem (at :).
  • Retorne as dimensões do conteúdo resultante em collectionViewContentSize . Assim, o coletor descobrirá onde a borda para a qual você pode rolar.

Na maioria dos dispositivos, o tamanho da pizza será de 300 pontos e as coordenadas e tamanhos de todas as células:



Fiz os cálculos em uma aula separada. Consiste em duas partes: calcula todos os quadros no construtor e, em seguida, fornece apenas acesso aos resultados finais:


 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]() } 

Em seguida, na classe de layout, você só precisa passar parâmetros do cache.


  1. O método de prepare chama o cálculo de todos os quadros.
  2. layoutAttributesForElements (em :) filtrará os quadros. Se o quadro cruzar com a área visível, a célula precisará ser exibida: calcule todos os atributos e retorne-o à matriz de células visíveis.
  3. layoutAttributesForItem (at :) - Calcula os atributos para uma única célula.

 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 } 

Mudamos de acordo com suas necessidades


Classificamos a visualização da tabela, mas agora precisamos criar um layout dinâmico. A cada troca de dedos, recalcularemos os atributos das células: pegue os quadros que já foram contados e altere-os usando .transform . Todas as alterações serão feitas em uma subclasse de PizzaHalfSelectorLayout .


Lemos o índice atual de pizza


Por conveniência, você pode esquecer o contentOffset e substituí-lo pelo número da pizza atual. Então você não precisará mais pensar em coordenadas, todas as decisões serão em torno do número da pizza e seu grau de deslocamento do centro da tela.


São necessários dois métodos: um converte contentOffset em um número de pizza e o outro vice-versa:


 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 } } 

O cálculo contentOffset para o centro da tela é renderizado em extension :


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

Paramos na pizza no centro


A primeira coisa que precisamos fazer é parar a pizza no centro da tela. O método targetContentOffset(forProposedContentOffset:) pergunta onde parar se, na velocidade atual, iria parar no proposedContentOffset .


O cálculo é simples: veja em qual pizza o proposedContentOffset se encaixará e role para que fique no 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 possui duas .normal rolagem: .normal e .normal . .fast mais adequado para .fast :


 collectionView!.decelerationRate = .fast 

Mas há um problema: se rolamos um pouco, precisamos permanecer na pizza e não pular para a próxima. Não existe um método para alterar a velocidade, portanto, o salto reverso, embora a uma pequena distância, mas com uma velocidade muito alta:



Cuidado, corte!


Se a célula não mudar, retornamos o contentOffset atual, para que a rolagem pare. Em seguida, nós mesmos iremos rolar para o local anterior usando o scrollToItem padrão. Infelizmente, você também precisa rolar de forma assíncrona, para que o código seja chamado após o return , e haverá pouco desbotamento durante a animação.


  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) } } 

O problema se foi, agora a pizza retorna sem problemas:



Aumentar Central Pizza


Recontamos o layout ao mover


É necessário fazer a pizza central aumentar gradualmente à medida que se aproxima do centro. Para fazer isso, você precisa calcular os parâmetros não uma vez no início, mas sempre no deslocamento. Liga simplesmente:


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

Agora, com cada deslocamento, os métodos prepare e layoutAttributesForElements(in:) serão chamados. Assim, podemos atualizar o UICollectionViewLayoutAttributes várias vezes seguidas, alterando suavemente a posição e a transparência.


Transformar células


No layout da tabela, as células estavam entre si e suas coordenadas foram contadas uma vez. Agora vamos alterá-los, dependendo da posição relativa ao centro da tela. Adicione um método que os altere rapidamente.


No método layoutAttributesForElements , layoutAttributesForElements precisa obter os atributos da superclasse, filtrar os atributos das células e passá-los para o 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) } 

Agora vamos mudar os atributos da célula em uma função:


  private func updateCells(_ cells: [UICollectionViewLayoutAttributes]) 

Durante o movimento, precisamos alterar a transparência, o tamanho e manter a pizza no centro.


A posição da célula em relação ao centro da tela é convenientemente apresentada em uma forma normalizada. Se a célula estiver no centro, o parâmetro será 0, se for deslocado, o parâmetro será alterado de -1 ao subir para 1 para 1. Se os valores estiverem mais distantes de zero que 1 / -1, isso significa que a célula não é mais central e parou de mudar. Eu chamei esse parâmetro de escala:



Você precisa calcular a diferença entre o centro do quadro e o centro da tela. Dividindo a diferença por uma constante, normalizamos o valor, e min e max levarão a um intervalo 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)) } } 

Tamanho


Tendo uma scale normalizada, você pode fazer qualquer coisa. As alterações de -1 a +1 são muito fortes, precisam ser convertidas para o tamanho. Por exemplo, queremos que o tamanho diminua para um máximo de 0,6 do tamanho da 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 redimensiona em relação ao centro das células. A célula central possui normScale = 0, seu tamanho não muda:



Transparência


A transparência pode ser alterada através do parâmetro alpha . O valor da scale que usamos na transform também é adequado.


  cell.alpha = scale 

Agora a pizza muda de tamanho e transparência. Já não é tão chato como nas tabelas regulares.



Bisectando


Antes disso, trabalhamos com uma pizza: definimos o sistema de referência do centro, alteramos o tamanho e a transparência. Agora você precisa dividir ao meio.


Usar uma coleção para isso é muito difícil: você precisará escrever seu próprio manipulador de gestos para cada metade. É mais fácil fazer duas coleções, cada uma com seu próprio layout. Só agora, em vez de uma pizza inteira, haverá metades.


Dois controladores, um contêiner


Quase sempre, eu quebro uma tela em vários UIViewController , cada um com sua própria tarefa. Desta vez, ficou assim:



  1. O controlador principal: todas as peças são montadas nele e o botão "mix".
  2. Controlador com dois contêineres para metades, uma assinatura central e indicadores de rolagem.
  3. O controlador com um coletor (branco direito).
  4. Painel inferior com preço.

Para distinguir entre a metade esquerda e a direita, iniciei o enum , ele é armazenado no layout na .orientation .orientation:


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

Mudamos as metades para o centro


O layout anterior parou de fazer o que esperamos: as metades começaram a mudar para o centro de suas coleções, não para o centro da tela:



A correção é simples: você precisa deslocar horizontalmente as células até a metade do centro da tela:


  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 } 

A distância entre as metades é imediatamente controlada.



Deslocamento da célula


Era fácil colocar pizza redonda em um quadrado e, pela metade, você precisa de meio quadrado:



Você pode reescrever o cálculo dos quadros: reduza pela metade a largura, alinhe os quadros ao centro de maneira diferente para cada metade. Para simplificar, basta alterar o contentMode imagem dentro da célula:


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


Pressione a pizza verticalmente


As pizzas diminuíram, mas a distância entre seus centros não mudou, grandes lacunas apareceram. Você pode compensá-los da mesma maneira que alinhamos as metades no 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 as compensações são assim:


  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) } } 

E a configuração da célula é assim:


  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 } } 

Não confunda: a configuração do quadro deve estar antes da transformação. Se você alterar a ordem, o resultado dos cálculos será completamente diferente.


Feito! Cortamos as metades e as alinhamos ao centro:



Adicionar legendas


Os cabeçalhos são criados da mesma maneira que as células, somente em vez de UICollectionViewLayoutAttributes(forCellWith:) você precisa usar o construtor UICollectionViewLayoutAttributes(forSupplementaryViewOfKind:)
e retorne-os junto com os parâmetros da célula em layoutAttributesForElements(in:)


Primeiro, descrevemos um método para obter um cabeçalho pelo 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 } 

O cálculo do quadro está oculto no método defaultFrameForHeader (a defaultFrameForHeader ).


Agora você pode obter o IndexPath células visíveis e mostrar os IndexPath para elas:


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

Uma chamada de função terrivelmente longa está oculta nos headers(for:) método:


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

zIndex


Agora, as células e assinaturas estão no mesmo nível de "altura", para que possam ser colocadas em camadas umas sobre as outras. Para manter os cabeçalhos sempre mais altos, dê-lhes zIndex maior que zero. Por exemplo, 100.


Fixamos uma posição (na verdade não)


As assinaturas fixadas na tela são um pouco intrigantes. Você deseja corrigir, mas vice-versa, mova-se constantemente com os bounds :



Tudo é simples no código: obtemos a posição da assinatura na tela e a mudamos para 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) } 

A altura das assinaturas pode ser diferente; é melhor considerá-lo no delegado (e no cache lá).


Animando assinaturas


Tudo é muito parecido com as células. Com base na scale atual, você pode calcular a transparência da célula. O deslocamento pode ser definido via .transform , para que a inscrição seja alterada em relação ao seu quadro:


  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) } } 


Otimizar


Depois de adicionar cabeçalhos, o desempenho diminuiu drasticamente. Isso aconteceu porque UICollectionViewLayoutAttributes as assinaturas, mas ainda as devolvemos para UICollectionViewLayoutAttributes . A partir disso, os cabeçalhos são adicionados à hierarquia, participam do layout, mas não são exibidos. Mostramos apenas as células que se cruzam com os bounds atuais e os cabeçalhos precisam ser filtrados por alpha :


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

Concordamos com a assinatura central (receita original)


Fizemos um ótimo trabalho, mas houve uma contradição na interface - se você escolher duas metades idênticas, elas se transformarão em uma pizza comum.


Decidimos deixá-lo assim, mas processamos corretamente a condição, mostrando que era uma pizza comum. Nossa nova tarefa é mostrar um rótulo no centro para pizzas idênticas e ocultar ao longo das bordas.



Somente o layout é muito difícil de resolver, porque a inscrição está na junção de dois coletores. Será mais fácil se o controlador, que contém as duas coleções, coordenar o movimento de todas as assinaturas.


Ao rolar, passamos o índice atual para o controlador, ele envia o índice para a metade oposta. Se os índices corresponderem, ele exibirá o título da pizza original e, se diferente, as assinaturas de cada metade serão visíveis.


Como inventar seus layouts


A parte mais difícil foi descobrir como colocar sua ideia na computação. Por exemplo, quero que as pizzas rolem como um tambor. Para entender o problema, realizei 4 etapas comuns:


  1. Eu desenhei alguns estados.
  2. Eu entendi como os elementos estão relacionados à posição da tela (os elementos se movem em relação ao centro da tela).
  3. Variáveis ​​criadas que são convenientes para trabalhar (centro da tela, moldura central da pizza, balança).
  4. Eu vim com etapas simples, cada uma das quais pode ser verificada.

Estados e animações são fáceis de desenhar no Keynote. Peguei o layout padrão e desenhei os dois primeiros passos:



O vídeo é assim:



Foram necessárias três alterações:


  1. Em vez de quadros do cache, usaremos centerPizzaFrame .
  2. Usando scale leia o deslocamento desse quadro.
  3. Recalcule o 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 } 


Depois que as células se sobrepõem, é necessário configurá-las na ordem correta com zIndex. A idéia é a seguinte: quanto mais próxima a célula da pizza central, mais próxima a tela e maior 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 } } 

Então, se a terceira célula estiver atual, ocorrerá assim:


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

Eu não obtive esse layout na produção, pois precisaria de fotos com fundo transparente.


Data de lançamento


Ao criar essa interface, nos envolvemos em um pequeno experimento: pela primeira vez, escrevemos um layout tão incomum e depois adicionamos uma transição interativa à tela do cartão. O experimento valeu a pena: as pizzas da metade entraram nas principais vendas e ficaram em segundo lugar, perdendo apenas para os pepperoni clássicos.


Obviamente, a produção precisava de mais trabalho:


  • corte imagens da pizza ao meio,
  • , : , ,
  • ,
  • ,
  • -,
  • «»,
  • Voice Over.

:



github , .


UICollectionViewLayout , A Tour of UICollectionView


, .

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


All Articles