Usando UIViewPropertyAnimator para criar animações personalizadas

Criar animações é ótimo. Eles são uma parte importante das Diretrizes da interface humana do iOS . As animações ajudam a chamar a atenção do usuário para coisas importantes ou simplesmente tornam o aplicativo menos chato.

Existem várias maneiras de implementar animação no iOS. Provavelmente, a maneira mais popular é usar UIView.animate (withDuration: animations :) . Você pode animar uma camada de imagem usando CABasicAnimation . Além disso, o UIKit permite configurar animações personalizadas para exibir o controlador usando o UIViewControllerTransitioningDelegate .

Neste artigo, quero discutir outra maneira interessante de animar visualizações - UIViewPropertyAnimator . Esta classe fornece muito mais funções de gerenciamento do que seu antecessor, UIView.animat e. Use-o para criar animações temporárias, interativas e interrompidas. Além disso, é possível alterar rapidamente o animador.

Apresentando o UIViewPropertyAnimator


O UIViewPropertyAnimator foi introduzido no iOS 10 . Permite criar animações de maneira orientada a objetos. Vejamos um exemplo de uma animação criada usando o UIViewPropertyAnimator .

imagem

Foi assim que aconteceu ao usar o UIView.

UIView.animate(withDuration: 0.3) { view.frame = view.frame.offsetBy(dx: 100, dy: 0) } 

E aqui está como fazer isso com o UIViewPropertyAnimator :

 let animator = UIViewPropertyAnimator(duration:0.3, curve: .linear) { view.frame = view.frame.offsetBy(dx:100, dy:0) } animator.startAnimation() 

Se você precisar verificar a animação, basta criar um Playground e executar o código como mostrado abaixo. Ambos os fragmentos de código levarão ao mesmo resultado.

imagem

Você pode pensar que neste exemplo não há muita diferença. Então, qual é o sentido de adicionar uma nova maneira de criar animações? O UIViewPropertyAnimator se torna mais útil quando você precisa criar animações interativas.

Animação interativa e interrompida


Você se lembra do gesto clássico "Passe o dedo para desbloquear o dispositivo"? Ou o gesto "Mova o dedo na tela de baixo para cima" para abrir o Centro de Controle? Estes são ótimos exemplos de animação interativa. Você pode começar a mover a imagem com o dedo, soltá-la e a imagem retornará à sua posição original. Além disso, você pode capturar a imagem durante a animação e continuar movendo-a com o dedo.

As animações do UIView não fornecem uma maneira fácil de controlar a porcentagem de conclusão da animação. Você não pode pausar a animação no meio de um loop e continuar sua execução após a interrupção.

Nesse caso, falaremos sobre UIViewPropertyAnimator . A seguir, veremos como você pode criar facilmente animação totalmente interativa, interrompida e animação reversa em algumas etapas.

Preparação do projeto de lançamento


Primeiro, você precisa baixar o projeto inicial . Depois de abrir o arquivo, você encontrará o aplicativo CityGuide que ajuda os usuários a planejar suas férias. O usuário pode rolar pela lista de cidades e abrir uma descrição detalhada com informações detalhadas sobre a cidade que ele gostou.

Considere o código fonte do projeto antes de começarmos a criar belas animações. Aqui está o que você pode encontrar em um projeto abrindo-o no Xcode :

  1. ViewController.swift : o controlador de aplicativo principal com um UICollectionView que exibe uma matriz de objetos City .
  2. CityCollectionViewCell.swift: célula para exibir a cidade . De fato, neste artigo, a maioria das alterações será aplicada a essa classe. Você pode perceber que descriptionLabel e closeButton já estão definidos na classe. No entanto, após iniciar o aplicativo, esses objetos serão ocultados. Não se preocupe, eles serão visíveis um pouco mais tarde. Essa classe também possui propriedades collectionView e index . Mais tarde, eles serão usados ​​para animação.
  3. CityCollectionViewFlowLayout.swift: essa classe é responsável pela rolagem horizontal. Nós não vamos mudar ainda.
  4. City.swift : O modelo principal de aplicativo possui um método usado no ViewController.
  5. Main.storyboard: Aqui você encontra a interface do usuário para o ViewController e CityCollectionViewCell .

Vamos tentar criar e executar o aplicativo de amostra. Como resultado, obtemos o seguinte.

cityguideapp-iphone8

Implementação de animação de implantação e recolhimento


Depois de iniciar o aplicativo, uma lista de cidades é exibida. Mas o usuário não pode interagir com objetos na forma de células. Agora você precisa exibir informações para cada cidade quando o usuário clica em uma das células. Dê uma olhada na versão final do aplicativo. Aqui está o que realmente precisava ser desenvolvido:

imagem

A animação parece boa, não é? Mas não há nada de especial aqui, é apenas a lógica básica do UIViewPropertyAnimator . Vamos ver como implementar esse tipo de animação. Crie um método collectionView (_: didSelectItemAt) , adicione o seguinte fragmento de código ao final do arquivo ViewController :

 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let selectedCell = collectionView.cellForItem(at: indexPath)! as! CityCollectionViewCell selectedCell.toggle() } 

Agora precisamos implementar o método de alternância . Vamos mudar para CityCollectionViewCell.swift e implementar esse método.

Primeiro, adicione a enumeração State na parte superior do arquivo, logo antes da declaração da classe CityCollectionViewCell . Esta listagem permite rastrear o estado de uma célula:

 private enum State { case expanded case collapsed var change: State { switch self { case .expanded: return .collapsed case .collapsed: return .expanded } } } 

Adicione algumas propriedades para controlar a animação na classe CityCollectionViewCell :

 private var initialFrame: CGRect? private var state: State = .collapsed private lazy var animator: UIViewPropertyAnimator = { return UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) }() 

A variável initialFrame é usada para armazenar o quadro da célula até que a animação seja executada . O estado é usado para rastrear se a célula é expandida ou recolhida. E a variável animador é usada para controlar a animação.

Agora adicione o método de alternância e chame-o do método close , por exemplo:

 @IBAction func close(_ sender: Any) { toggle() } func toggle() { switch state { case .expanded: collapse() case .collapsed: expand() } } 

Em seguida, adicionamos mais dois métodos: expand () e collapse () . Continuaremos sua implementação. Primeiro, começamos com o método expansiond () :

 private func expand() { guard let collectionView = self.collectionView, let index = self.index else { return } animator.addAnimations { self.initialFrame = self.frame self.descriptionLabel.alpha = 1 self.closeButton.alpha = 1 self.layer.cornerRadius = 0 self.frame = CGRect(x: collectionView.contentOffset.x, y:0, width: collectionView.frame.width, height: collectionView.frame.height) if let leftCell = collectionView.cellForItem(at: IndexPath(row: index - 1, section: 0)) { leftCell.center.x -= 50 } if let rightCell = collectionView.cellForItem(at: IndexPath(row: index + 1, section: 0)) { rightCell.center.x += 50 } self.layoutIfNeeded() } animator.addCompletion { position in switch position { case .end: self.state = self.state.change collectionView.isScrollEnabled = false collectionView.allowsSelection = false default: () } } animator.startAnimation() } 

Quanto código. Deixe-me explicar o que está acontecendo passo a passo:

  1. Primeiro, verifique se collectionView e index não são iguais a zero. Caso contrário, não poderemos iniciar a animação.
  2. Em seguida, comece a criar a animação chamando animator.addAnimations .
  3. Em seguida, salve o quadro atual, que é usado para restaurá-lo na animação de convolução.
  4. Em seguida, definimos o valor alfa para descriptionLabel e closeButton para torná-los visíveis.
  5. Em seguida, remova o canto arredondado e defina um novo quadro para a célula. A célula será mostrada em tela cheia.
  6. Em seguida, movemos as células vizinhas.
  7. Agora chame o método animator.addComplete () para desativar a interação da imagem da coleção. Isso evita que os usuários rolem durante a expansão celular. Altere também o estado atual da célula. É importante alterar o estado da célula e somente depois disso a animação termina.

Agora adicione uma animação de convolução. Em resumo, apenas restauramos a célula ao seu estado anterior:

 private func collapse() { guard let collectionView = self.collectionView, let index = self.index else { return } animator.addAnimations { self.descriptionLabel.alpha = 0 self.closeButton.alpha = 0 self.layer.cornerRadius = self.cornerRadius self.frame = self.initialFrame! if let leftCell = collectionView.cellForItem(at: IndexPath(row: index - 1, section: 0)) { leftCell.center.x += 50 } if let rightCell = collectionView.cellForItem(at: IndexPath(row: index + 1, section: 0)) { rightCell.center.x -= 50 } self.layoutIfNeeded() } animator.addCompletion { position in switch position { case .end: self.state = self.state.change collectionView.isScrollEnabled = true collectionView.allowsSelection = true default: () } } animator.startAnimation() } 

Agora é hora de compilar e executar o aplicativo. Tente clicar na célula e você verá a animação. Para fechar a imagem, clique no ícone de cruz no canto superior direito.

Adicionando processamento de gestos


Você pode reivindicar atingir o mesmo resultado usando UIView.animate . Qual é o sentido de usar um UIViewPropertyAnimator ?

Bem, é hora de tornar a animação interativa. Adicione um UIPanGestureRecognizer e uma nova propriedade chamada popupOffset para rastrear quanto a célula pode ser movida. Vamos declarar essas variáveis ​​na classe CityCollectionViewCell :

 private let popupOffset: CGFloat = (UIScreen.main.bounds.height - cellSize.height)/2.0 private lazy var panRecognizer: UIPanGestureRecognizer = { let recognizer = UIPanGestureRecognizer() recognizer.addTarget(self, action: #selector(popupViewPanned(recognizer:))) return recognizer }() 

Em seguida, adicione o seguinte método para registrar a definição de furto:

 override func awakeFromNib() { self.addGestureRecognizer(panRecognizer) } 

Agora você precisa adicionar o método popupViewPanned para rastrear o gesto de furto. Cole o seguinte código no CityCollectionViewCell :

 @objc func popupViewPanned(recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: toggle() animator.pauseAnimation() case .changed: let translation = recognizer.translation(in: collectionView) var fraction = -translation.y / popupOffset if state == .expanded { fraction *= -1 } animator.fractionComplete = fraction case .ended: animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) default: () } } 

Existem três estados. No início do gesto, inicializamos o animador usando o método de alternância e pausamos imediatamente. Enquanto o usuário arrasta a célula, atualizamos a animação definindo as propriedades do multiplicador de fraçãoCompleto. Essa é a principal mágica do animador, que permite controlar. Finalmente, quando o usuário solta o dedo, o método do animador continueAnimation é chamado para continuar a animação. Então a célula se moverá para a posição de destino.

Depois de iniciar o aplicativo, você pode arrastar a célula para cima para expandi-la. Em seguida, arraste a célula expandida para baixo para recolhê-la.

A animação agora parece muito boa, mas não é possível interromper a animação no meio. Portanto, para tornar a animação totalmente interativa, você precisa adicionar outra função - interrupção. O usuário pode iniciar a animação de expansão / recolhimento como de costume, mas a animação deve ser pausada imediatamente após o usuário clicar na célula durante o ciclo de animação.

Para fazer isso, você precisa salvar o progresso da animação e levar esse valor em consideração para calcular a porcentagem de conclusão da animação.

Primeiro, declare uma nova propriedade no CityCollectionViewCell :

 private var animationProgress: CGFloat = 0 

Atualize o bloco .began do método popupViewPanned com a seguinte linha de código para lembrar o progresso:

 animationProgress = animator.fractionComplete 

No bloco .changed , você precisa atualizar a seguinte linha de código para calcular corretamente a porcentagem de conclusão:

 animator.fractionComplete = fraction + animationProgress 

Agora o aplicativo está pronto para teste. Execute o projeto e veja o que acontece. Se todas as ações forem executadas corretamente, seguindo minhas instruções, a animação deverá se parecer com:

imagem

Animação reversa


Você pode encontrar uma falha para a implementação atual. Se você arrastar a célula um pouco e depois retorná-la à sua posição original, a célula continuará a se expandir quando você soltar o dedo. Vamos corrigir esse problema para tornar a animação interativa ainda melhor.
Vamos atualizar o bloco .end do método popupViewPanned , conforme descrito abaixo:

 let velocity = recognizer.velocity(in: self) let shouldComplete = velocity.y > 0 if velocity.y == 0 { animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) break } switch state { case .expanded: if !shouldComplete && !animator.isReversed { animator.isReversed = !animator.isReversed } if shouldComplete && animator.isReversed { animator.isReversed = !animator.isReversed } case .collapsed: if shouldComplete && !animator.isReversed { animator.isReversed = !animator.isReversed } if !shouldComplete && animator.isReversed { animator.isReversed = !animator.isReversed } } animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) 

Agora, levamos em conta a velocidade do gesto para determinar se a animação deve ser revertida.

E, finalmente, insira outra linha de código no bloco .changed . Coloque esse código à direita do cálculo animator.fractionComplete .

 if animator.isReversed { fraction *= -1 } 

Vamos executar o aplicativo novamente. Agora tudo deve funcionar sem falhas.

imagem

Corrigir gesto de panorâmica


Portanto, concluímos a implementação da animação usando o UIViewPropertyAnimator . No entanto, há um erro desagradável. Você pode tê-la conhecido enquanto testava o aplicativo. O problema é que não é possível rolar a célula horizontalmente. Vamos tentar deslizar para a esquerda / direita pelas células e somos confrontados com o problema.

O principal motivo está relacionado ao UIPanGestureRecognizer que criamos . Ele também captura um gesto de furto e entra em conflito com o reconhecedor de gestos interno UICollectionView .

Embora o usuário ainda possa rolar pela parte superior / inferior das células ou pelo espaço entre as células para rolar pelas cidades, ainda não gosto de uma interface do usuário tão ruim. Vamos consertar.

Para resolver conflitos, precisamos implementar um método delegado chamado gestRecognizerShouldBegin (_ :) . Este método controla se o reconhecedor de gestos deve continuar a interpretar os toques. Se você retornar falso no método, o reconhecedor de gestos ignorará os toques. Então, o que vamos fazer é dar à nossa própria ferramenta de reconhecimento de panorama a capacidade de ignorar movimentos horizontais.

Para fazer isso, vamos definir o delegado do nosso reconhecedor de pan. Insira a seguinte linha de código na inicialização do panRecognizer (você pode colocar o código antes do reconhecedor de retorno :

 recognizer.delegate = self 

Em seguida, implementamos o método gestRecognizerShouldBegin (_ :) da seguinte maneira:

 override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return abs((panRecognizer.velocity(in: panRecognizer.view)).y) > abs((panRecognizer.velocity(in: panRecognizer.view)).x) } 

Abriremos / fecharemos se sua velocidade vertical for maior que a velocidade horizontal.

Uau! Vamos testar o aplicativo novamente. Agora você pode percorrer a lista de cidades deslizando para a esquerda / direita pelas células.

imagem

Bônus: Recursos de sincronização personalizada


Antes de terminar este tutorial, vamos falar sobre as funções de temporização. Você ainda se lembra do caso em que o desenvolvedor solicitou a implementação de uma função de sincronização personalizada para a animação que você criou?

Normalmente, você deve alterar o UIView.animation para CABasicAnimation ou envolvê-lo na transação de CAT . Com o UIViewPropertyAnimator, você pode implementar facilmente funções de tempo personalizadas.

As funções de tempo (ou funções de facilitação) são entendidas como funções de velocidade da animação que afetam a taxa de alteração de uma ou outra propriedade animada. Atualmente, há quatro tipos suportados: facilidadeInOut, facilidadeIn, facilidadeOut, linear.

Substitua a inicialização do animador por essas funções de tempo (tente desenhar sua própria curva cúbica de Bezier) da seguinte maneira:

 private lazy var animator: UIViewPropertyAnimator = { let cubicTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.17, y: 0.67), controlPoint2: CGPoint(x: 0.76, y: 1.0)) return UIViewPropertyAnimator(duration: 0.3, timingParameters: cubicTiming) }() 

Como alternativa, em vez de usar parâmetros de sincronização cúbica, você também pode usar a sincronização de mola, por exemplo:

  let springTiming = UISpringTimingParameters(mass: 1.0, stiffness: 2.0, damping: 0.2, initialVelocity: .zero) 

Tente iniciar o projeto novamente e veja o que acontece.

Conclusão


Com o UIViewPropertyAnimator, você pode aprimorar as telas estáticas e a interação do usuário por meio de animações interativas.

Eu sei que você não pode esperar para perceber o que aprendeu em seu próprio projeto. Se você aplicar essa abordagem em seu projeto, será muito legal, deixe-me saber sobre isso, deixando um comentário abaixo.

Como referência, aqui você pode baixar o rascunho final .

Links adicionais


Animações profissionais usando o UIKit - https://developer.apple.com/videos/play/wwdc2017/230/

Documentação do UIViewPropertyAnimator para desenvolvedores da Apple - https://developer.apple.com/documentation/uikit/uiviewpropertyanimator

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


All Articles