Mergulhe mais fundo no Motion Design: transições avançadas para iOS

Publicados: 2020-11-09

A criatividade traz consigo um desejo constante de impressionar o usuário. Durante séculos, o homem tentou manipular os meios disponíveis para recriar a interação mais natural, tomando a natureza como exemplo fundamental.

Ao descobrir o mundo, uma pessoa se torna cada vez mais sensível aos detalhes sutis do mundo ao seu redor, o que lhe permite distinguir instintivamente a artificialidade dos seres vivos. Essa linha se confunde com o desenvolvimento da tecnologia, onde o software visa criar um ambiente em que seu usuário descreva sua experiência em um mundo criado artificialmente como natural.

Ecoando a natureza no design do aplicativo

Este artigo apresentará o processo de desfoque da borda usando o exemplo de transformação de forma na animação interativa de elementos cotidianos na maioria dos aplicativos iOS. Uma maneira de imitar a natureza é através de várias transformações da posição de um objeto no tempo. Funções exemplificativas de tempo de animação são apresentadas abaixo.

Combinado com o uso correto do tempo , adicionando uma transformação geométrica, podemos obter um número infinito de efeitos . Como demonstração das possibilidades de design e tecnologia atuais, foi criado o aplicativo Motion Patterns, que inclui soluções populares, desenvolvidas por nossa empresa de desenvolvimento de software. Como não sou escritor, mas programador, e nada fala melhor do que exemplos ao vivo, não tenho escolha a não ser convidá-lo para este mundo maravilhoso!

Amostras para descobrir

Vamos dar uma olhada em como o design de movimento pode transformar um design comum em algo excepcional! Nos exemplos abaixo, do lado esquerdo há um aplicativo que usa apenas animações básicas do iOS, enquanto do lado direito há uma versão do mesmo aplicativo com algumas melhorias.

Efeito “Mergulhe mais fundo”

Esta é uma transição usando a transformação entre dois estados de uma visão . Construído com base em uma coleção, após a seleção de uma célula específica, a transição para os detalhes de um elemento ocorre transformando seus elementos individuais *. Uma solução adicional é o uso de transições interativas, que facilitam o uso do aplicativo.

*realmente copiando / mapeando elementos de dados em uma visão temporária participando da transição, entre seu início e seu fim … mas explicarei isso mais adiante neste artigo …

Efeito “Espreitar por cima da borda”

O uso da animação de visualização de rolagem em sua ação transforma a imagem na forma de 3D para um efeito de cubo . O principal fator responsável pelo efeito é o deslocamento da visualização de rolagem.

Efeito “Ligue os pontos”

Esta é uma transição entre as cenas que transforma o objeto em miniatura na tela inteira . As coleções utilizadas para este fim funcionam concomitantemente, uma mudança em uma tela corresponde a uma mudança na outra. Além disso, quando você insere a miniatura em segundo plano, um efeito de paralaxe aparece quando você desliza entre as cenas.

Efeito “Mudar a Forma”

O último tipo de animação é simples usando a biblioteca Lottie. É o uso mais comum para animar ícones. Nesse caso, esses são os ícones na barra de guias. Além disso, alterando as abas apropriadas, uma animação da transição em uma direção específica foi usada para intensificar ainda mais o efeito de interação.

Mergulhe mais fundo: nosso primeiro padrão de design de movimento

Agora é hora de ir direto ao ponto… precisamos nos aprofundar ainda mais na estrutura dos mecanismos que controlam esses exemplos.

Neste artigo, apresentarei o primeiro padrão de design de movimento , que chamamos de 'Dive Deeper' com uma descrição abstrata de seu uso, sem entrar em detalhes específicos. Planejamos disponibilizar o código exato e todo o repositório para todos no futuro, sem quaisquer restrições.

A arquitetura do projeto e os padrões de design de programação estritos aplicados não são uma prioridade neste momento - nos concentramos em animações e transição.

Neste artigo, usaremos dois conjuntos de recursos fornecidos para gerenciar visualizações durante as transições de cena. Assim, gostaria de salientar que este artigo é destinado a pessoas que estão relativamente familiarizadas com o UIKit e a sintaxe do Swift.

https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

https://developer.apple.com/documentation/uikit/uipercentdriveninteractivetransition

https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html

Primeiro: a estrutura

Para a versão básica que implementa uma determinada solução, serão necessárias várias classes auxiliares, responsáveis ​​por fornecer as informações necessárias sobre as visões envolvidas na transição, e controlar a própria transição e as interações.

Transição em Swift

A classe básica responsável por gerenciar a transição, o proxy, será TransitionAnimation. Ele decide como a transição ocorrerá e abrange as funções padrão necessárias para executar a ação fornecida pela equipe da Apple.

 /// Esta é uma classe fundamental para transições que têm comportamento diferente ao apresentar e dispensar em uma duração especificada.
classe aberta TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    /// Indicando se está apresentando ou dispensando a transição.
    var apresentando: Bool = true
    
    /// Intervalo de tempo em que toda a transição ocorre.
    duração do let privado: TimeInterval
    
    /// Inicializador padrão do animador de transição com valores padrão.
    /// - Duração do parâmetro: intervalo de tempo em que ocorre toda a transição.
    /// - Parâmetro apresentando: Indicador se está apresentando ou dispensando a transição.
    public init(duração: TimeInterval = 0,5, apresentando: Bool = true) {
        self.duration = duração
        self.presenting = apresentando
        super.init()
    }
    
    /// Determina a duração da transição.
    /// - Parâmetro transitionContext: Contexto da transição atual.
    /// - Retorna: Duração especificada na inicialização do animador.
    public func transitionDuration(usando transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        retornar self.duration
    }
    
    /// Coração do animador de transição, nesta função ocorre a transição.
    /// - Importante: Sobrescrever esta função em um tipo de transição mais concreto é crucial para realizar animações.
    /// - Parâmetro transitionContext: Contexto de transição.
    public func animateTransition(usando transitionContext: UIViewControllerContextTransitioning) { }
    
}

Com base no TransitionAnimator, criamos um arquivo TransformTransition, cuja tarefa será realizar uma transição específica, transição com transformação (parenting)

 /// Implementação da transição de transformação.
classe aberta TransformTransition: TransitionAnimator {
    
    /// Visualiza o modelo que contém todas as especificações de transição necessárias.
    private var viewModel: TransformViewModel
    
    /// Inicializador padrão da transição de transformação.
    /// - Parâmetro viewModel: Visualiza o modelo de transição de transformação.
    /// - Duração do parâmetro: Duração da transição.
    init(viewModel: TransformViewModel, duração: TimeInterval) {
        self.viewModel = viewModel
        super.init(duração: duração, apresentação: viewModel.presenting)
    }

A composição da classe TransformTransition inclui o TransformViewModel, que, como o nome sugere, informa o mecanismo de quais modelos de visualização essa transição será aplicada.

 /// Visualiza o modelo de transição de transformação que contém informações básicas sobre ela.
classe final TransformViewModel {
    
    /// Indica se a transição de transformação está apresentando ou dispensando a visualização.
    vamos apresentar: Bool
    /// Array de modelos com especificação de transform para cada view.
    deixe modelos: [TransformModel]
    
    /// Inicializador padrão do modelo de exibição de transformação.
    /// - Apresentação do parâmetro: Indica se está apresentando ou dispensando a transição de transformação.
    /// - Modelos de parâmetros: Array de modelos com especificação de transformação para cada visualização.
    init(apresentando: Bool, models: [TransformModel]) {
        self.presenting = apresentando
        self.models = modelos
    }
    
}

O modelo de transformação é uma classe auxiliar que descreve os elementos específicos das visões envolvidas na transição localizada no pai, geralmente as visões de um controlador que podem ser transformadas.

No caso de uma transição, é um passo necessário porque esta transição consiste nas operações de visões específicas entre determinados estados.

Segundo: a implementação

Estendemos o modelo de visão a partir do qual iniciamos a transição com Transformable, o que nos obriga a implementar uma função que preparará todos os elementos necessários. O tamanho desta função pode crescer muito rapidamente, então sugiro que você a divida em partes menores, por exemplo, por elemento.

 /// Protocolo para a classe que deseja realizar a transição de transformação.
protocolo transformável: ViewModel {
    
    /// Prepara modelos de visões que estão envolvidas na transição.
    /// - Parâmetro fromView: A visão a partir da qual a transição começa
    /// - Parâmetro toView: A visão para a qual a transição vai.
    /// - Parâmetro apresentando: Indica se está apresentando ou dispensando.
    /// - Returns: Array de estruturas que contém todas as informações necessárias prontas para transformar a transição para cada view.
    func prepareTransitionModels(fromView: UIView, toView: UIView, apresentando: Bool) -> [TransformModel]
    
}

A suposição não é dizer como pesquisar os dados das visualizações que participam da transformação. No meu exemplo usei tags que representam uma determinada view. Você tem uma mão livre nesta parte da implementação.

Os modelos de transformações de visão específicas (TransformModel) são o menor modelo de toda a lista. Eles consistem em informações chave de transformação, como visualização inicial, visualização de transição, quadro inicial, quadro final, centro inicial, centro final, animações simultâneas e operação final. A maioria dos parâmetros não precisa ser usada durante a transformação, portanto, eles têm seus próprios valores padrão. Para resultados mínimos, basta usar apenas os necessários.

 /// Inicializador padrão do modelo de transformação com valores padrão.
    /// - Parâmetro initialView: Vista a partir da qual a transição começa.
    /// - Parâmetro phantomView: Visualização que é apresentada durante a transição de transformação.
    /// - Parâmetro initialFrame: Quadro de visão que inicia a transição de transformação.
    /// - Parâmetro finalFrame: Quadro de visão que será apresentado ao final da transição de transformação.
    /// - Parâmetro initialCenter: Necessário quando o ponto de vista inicial do centro é diferente do centro da vista inicial.
    /// - Parâmetro finalCenter: Necessário quando o ponto de vista do centro final é diferente do centro da vista final.
    /// - Parâmetro parallelAnimation: Animação adicional da visualização realizada durante a transição de transformação.
    /// - Conclusão do parâmetro: Bloco de código acionado após a transição de transformação.
    /// - Nota: Apenas a visualização inicial é necessária para executar a versão mais minimalista da transição de transformação.
    init(initialView: UIView,
         phantomView: UIView = UIView(),
         inicialFrame: CGRect = CGRect(),
         finalFrame: CGRect = CGRect(),
         inicialCenter: CGPoint? = nada,
         finalCenter: CGPoint? = nada,
         animação paralela: (() -> Void)? = nada,
         conclusão: (() -> Vazio)? = zero) {
        self.initialView = initialView
        self.phantomView = phantomView
        self.initialFrame = initialFrame
        self.finalFrame = finalFrame
        self.parallelAnimation = parallelAnimation
        self.completion = conclusão
        self.initialCenter = initialCenter ?? CGPoint(x: InitialFrame.midX, y: InitialFrame.midY)
        self.finalCenter = finalCenter ?? CGPoint(x: finalFrame.midX, y: finalFrame.midY)
    }

Sua atenção pode ter sido capturada pela visualização fantasma. Este é o momento em que explicarei o fluxo de trabalho para transições do iOS. Da forma mais curta possível…

O fluxo de trabalho para transições do iOS

Quando o usuário deseja passar para a próxima cena, o iOS prepara controladores específicos copiando os controladores inicial (azul) e alvo (verde) para a memória. Em seguida, é criado um contexto de transição através do coordenador de transição que contém o container, uma visão 'estúpida' que não contém nenhuma função especial, além de simular as visões de transição entre as duas cenas.

O princípio-chave de trabalhar com transições é não adicionar nenhuma visualização real ao contexto de transição, porque no final da transição, todo o contexto é desalocado, juntamente com as visualizações adicionadas ao contêiner. Essas são visualizações que existem apenas durante a transição e são removidas.

Portanto, o uso de phantom views que são réplicas de views reais é uma solução importante para essa transição.

Neste caso, temos uma transição que transforma uma visão em outra alterando sua forma e tamanho. Para fazer isso, no início da transição, crio um PhantomView do elemento fornecido e o adiciono ao contêiner. FadeView é uma visualização auxiliar para adicionar suavidade à transição geral.

 /// Coração da transição de transformação, onde a transição é executada. Substituição de `TransitionAnimator.animateTransition(...)`.
    /// - Parâmetro transitionContext: O contexto da transição de transformação atual.
    substituir função aberta animateTransition(usando transitionContext: UIViewControllerContextTransitioning) {
        guard let toViewController = transitionContext.view(forKey: .to),
            let fromViewController = transitionContext.view(forKey: .from) else {
                return Log.unexpectedState()
        }
        deixe containerView = transitionContext.containerView
        let duração = transiçãoDuração(usando: transitionContext)
        let fadeView = toViewController.makeFadeView(opacity: (!presenting).cgFloatValue)
        deixe modelos = viewModel.models
        deixe apresentadoVisualização = apresentando? toViewController : fromViewController
        
        models.forEach { $0.initialView.isHidden = true }
        apresentadoView.isHidden = true
        containerView.addSubview(toViewController)
        se apresentar {
            containerView.insertSubview(fadeView, belowSubview: toViewController)
        } senão {
            containerView.addSubview(fadeView)
        }
        containerView.addSubviews(viewModel.models.map { $0.phantomView })

Na próxima etapa, transformo-o na forma de destino por meio de transformações e, dependendo de ser uma apresentação ou um recall, ele realiza operações adicionais para limpar visualizações específicas – essa é toda a receita para essa transição.

 deixe animações: () -> Void = { [weak self] in
            guard let self = self else { return Log.unexpectedState() }
            fadeView.alpha = self.presenting.cgFloatValue
            models.forEach {
                deixe centro = self.presenting ? $0.finalCenter : $0.initialCenter
                deixe transformar = self.presenting ? $0.presentTransform : $0.dismissTransform
                $0.phantomView.setTransformAndCenter(transform, center)
            }
            models.compactMap { $0.parallelAnimation }.forEach { $0() }
        }
        
        let complete: (Bool) -> Void = { _ in
            transiçãoContext.completeTransition(!transitionContext.transitionWasCancelled)
            apresentadoView.isHidden = false
            models.compactMap { $0.completion }.forEach { $0() }
            models.forEach { $0.initialView.isHidden = false }
            if !self.presenting && transitionContext.transitionWasCancelled {
                toViewController.removeFromSuperview()
                fadeView.removeFromSuperview()
            }
        }
        
        UIView.animate(withDuration: duração,
                       atraso: 0,
                       usandoSpringWithDamping: 1,
                       inicialSpringVelocity: 0,5,
                       opções: .curveEaseOut,
                       animações: animações,
                       conclusão: conclusão)

Terceiro: o ingrediente especial

Depois de juntar todas as funções, classes e protocolos, o resultado deve ficar assim:

O componente final de nossa transição será sua plena interatividade. Para isso, usaremos um Pan Gesture adicionado na visão do controlador, TransitionInteractor…

 /// Mediador para lidar com a transição interativa.
classe final TransitionInteractor: UIPercentDrivenInteractiveTransition {
    
    /// Indica se a transição começou.
    var hasStarted = false
    /// Indica se a transição deve terminar.
    var deveTerminar = false

}

… que também inicializamos no corpo do controlador.

 /// Manipula o gesto panorâmico em itens de exibição de coleção e gerencia a transição.
    @objc func handlePanGesture(_ gestoRecognizer: UIPanGestureRecognizer) {
        let percentThreshold: CGFloat = 0,1
        let translation = gestoRecognizer.translation(in: view)
        deixe verticalMovement = translation.y / view.bounds.height
        deixe movimento ascendente = fminf(Float(movimento vertical), 0.0)
        deixeMovimentoPara cimaPercent = fminf(abs(Movimento para cima), 0,9)
        deixe progresso = CGFloat(upwardMovementPercent)
        guard let interator = interaçãoController else { return }
        alternar gestoRecognizer.state {
        caso .começou:
            interactor.hasStarted = true
            let tapPosition = gestoRecognizer.location(in: collectionView)
            showDetailViewControllerFrom(local: tapPosition)
        caso .alterado:
            interactor.shouldFinish = progresso > percentThreshold
            interactor.update(progresso)
        caso .cancelado:
            interactor.hasStarted = false
            interator.cancel()
        caso .terminado:
            interactor.hasStarted = false
            interator.shouldFinish
                ? interator.finish()
                : interator.cancel()
        predefinição:
            parar
        }
    }

A interação pronta deve ser a seguinte:

Se tudo correr conforme o planejado, nosso aplicativo ganhará muito mais aos olhos de seus usuários.

Descobriu apenas um pico de um iceberg

Da próxima vez, explicarei a implementação de questões relacionadas ao design de movimento nas próximas edições.

A aplicação, design e código fonte são propriedade da Miquido, e foram criados com paixão por talentosos designers e programadores cujo uso não nos responsabilizamos nas nossas implementações. O código-fonte detalhado estará disponível no futuro por meio de nossa conta no github - convidamos você a nos seguir!

Obrigado pela atenção e até breve!