1. Introdução
O Material Design é um sistema para criar produtos digitais bonitos e arrojados. Ao combinar estilo, branding, interação e movimento em um conjunto consistente de princípios e componentes, as equipes de produto podem atingir o maior potencial de design.
Os componentes do Material Design (MDC, na sigla em inglês) ajudam os desenvolvedores a implementar o Material Design. Criados por uma equipe de engenheiros e designers de UX do Google, os MDC oferecem dezenas de componentes de IU bonitos e funcionais disponíveis para Android, iOS, Web e Flutter. material.io/develop |
O que é o sistema de movimento do Material Design para o Flutter?
O sistema de movimento do Material Design para o Flutter é um conjunto de padrões de transição do pacote de animações. Ele ajuda os usuários a entender e navegar em um app, conforme descrito nas diretrizes do Material Design.
Veja os quatro padrões principais de transição do Material Design:
- Transformação de contêiner: faz a transição entre elementos da IU que incluem um contêiner. Cria uma conexão visível entre dois elementos ao transformar um no outro.
- Eixo compartilhado: faz a transição entre elementos da IU que têm uma relação espacial ou de navegação. Usa uma transformação compartilhada nos eixos x, y ou z para reforçar a relação entre os elementos.
- Esmaecimento cruzado: faz a transição entre elementos da IU que não têm uma relação forte entre si. Usa o esmaecimento e a exibição gradual de forma sequencial com uma escala do elemento de entrada.
- Esmaecimento: usado para elementos da IU que entram ou saem dos limites da tela.
O pacote de animações oferece widgets de transição para esses padrões, criados com base na biblioteca de animações do Flutter (flutter/animation.dart
) e na biblioteca do Material Design para o Flutter (flutter/material.dart
) (links em inglês):
Neste codelab, você usará as transições do Material Design com base no framework do Flutter e na biblioteca do Material Design, ou seja, você trabalhará com widgets. :)
O que você criará
Este codelab orientará você na criação de algumas transições em um app de e-mails de exemplo do Flutter chamado Reply. Usaremos o Dart para demonstrar como usar as transições do pacote de animações para personalizar a aparência e o uso do app.
Você receberá o código inicial do app Reply para incorporar nele as seguintes transições do Material Design, que podem ser vistas no GIF do codelab completo abaixo:
- Transição de transformação de contêiner da lista de e-mails para a página de detalhes
- Transição de transformação de contêiner do FAB para a página "Escrever e-mail"
- Transição de eixo z compartilhado do ícone de pesquisa para a página de visualização de pesquisa
- Transição de esmaecimento cruzado entre as páginas da caixa de e-mails
- Transição de esmaecimento cruzado entre o FAB de escrever e o de responder
- Transição de esmaecimento cruzado entre títulos da caixa de e-mails que desaparecem
- Transição de esmaecimento cruzado entre as ações da barra de apps inferior
Pré-requisitos
- Conhecimento básico de desenvolvimento no Flutter e Dart
- Um editor de código
- Um emulador ou dispositivo Android/iOS
- O exemplo de código (confira a próxima etapa)
Como você classificaria seu nível de experiência na criação de apps Flutter?
O que você quer aprender com este codelab?
2. Configurar o ambiente de desenvolvimento do Flutter
Você precisa de dois softwares para concluir este laboratório: o SDK do Flutter e um editor.
É possível executar o codelab usando qualquer um destes dispositivos:
- Um dispositivo físico Android ou iOS conectado ao seu computador e configurado para o modo de desenvolvedor.
- O simulador para iOS, que exige a instalação de ferramentas do Xcode.
- O Android Emulator, que requer configuração no Android Studio.
- Um navegador (o Chrome é necessário para depuração).
- Como um aplicativo para computador Windows, Linux ou macOS. Você precisa desenvolver na plataforma em que planeja implantar. Portanto, se quiser desenvolver um app para um computador Windows, você terá que desenvolver no Windows para acessar a cadeia de compilação adequada. Há requisitos específicos de cada sistema operacional que são abordados em detalhes em docs.flutter.dev/desktop.
3. Fazer o download do app inicial do codelab
Opção 1: clonar o app inicial do codelab no GitHub
Para clonar este codelab no GitHub, execute estes comandos:
git clone https://github.com/material-components/material-components-flutter-motion-codelab.git cd material-components-flutter-motion-codelab
Opção 2: fazer o download do arquivo ZIP do app inicial do codelab
O app inicial está localizado no diretório material-components-flutter-motion-codelab-starter
.
Verificar as dependências do projeto
O projeto depende do pacote de animações. Em pubspec.yaml
, verifique se a seção dependencies
inclui o seguinte:
animations: ^2.0.0
Abrir o projeto e executar o app
- Abrir o projeto no seu editor favorito.
- Siga as instruções para "Executar o app" em Get Started: Test drive no editor escolhido.
Pronto. O código inicial da página inicial do Reply será executado no dispositivo/emulador. Você vai acessar a Caixa de entrada com uma lista de e-mails.
Opcional: tornar as animações do dispositivo mais lentas
Como este codelab envolve transições rápidas, mas refinadas, pode ser útil desacelerar as animações do dispositivo para observar alguns dos detalhes mais delicados das transições durante a implementação. Isso pode ser feito com uma configuração no app, que pode ser acessada por um toque no ícone de configurações quando a gaveta inferior está aberta. Não se preocupe, esse método de desaceleramento de animações do dispositivo não afetará as animações no dispositivo fora do app Reply.
Opcional: modo escuro
Se o tema claro do Reply está incomodando seus olhos, a solução chegou. Há uma configuração incluída no app que permite mudar o tema para o modo escuro, para que haja mais conforto para seus olhos. Para acessar essa configuração, toque no ícone de configurações quando a gaveta inferior estiver aberta.
4. Conhecer o código do app de exemplo
Vamos analisar o código. Fornecemos um app que usa o pacote de animações (link em inglês) para fazer transições entre diferentes telas no aplicativo.
- HomePage: exibe a caixa de e-mails selecionada.
- InboxPage: exibe uma lista de e-mails.
- MailPreviewCard: exibe a visualização de um e-mail.
- MailViewPage: exibe um único e-mail completo.
- ComposePage: permite escrever um novo e-mail.
- SearchPage: exibe uma visualização de pesquisa.
router.dart
Primeiro, para entender como a navegação raiz do app é configurada, abra o router.dart
no diretório lib
:
class ReplyRouterDelegate extends RouterDelegate<ReplyRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<ReplyRoutePath> {
ReplyRouterDelegate({required this.replyState})
: navigatorKey = GlobalObjectKey<NavigatorState>(replyState) {
replyState.addListener(() {
notifyListeners();
});
}
@override
final GlobalKey<NavigatorState> navigatorKey;
RouterProvider replyState;
@override
void dispose() {
replyState.removeListener(notifyListeners);
super.dispose();
}
@override
ReplyRoutePath get currentConfiguration => replyState.routePath!;
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<RouterProvider>.value(value: replyState),
],
child: Selector<RouterProvider, ReplyRoutePath?>(
selector: (context, routerProvider) => routerProvider.routePath,
builder: (context, routePath, child) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
const CustomTransitionPage(
transitionKey: ValueKey('Home'),
screen: HomePage(),
),
if (routePath is ReplySearchPath)
const CustomTransitionPage(
transitionKey: ValueKey('Search'),
screen: SearchPage(),
),
],
);
},
),
);
}
bool _handlePopPage(Route<dynamic> route, dynamic result) {
// _handlePopPage should not be called on the home page because the
// PopNavigatorRouterDelegateMixin will bubble up the pop to the
// SystemNavigator if there is only one route in the navigator.
assert(route.willHandlePopInternally ||
replyState.routePath is ReplySearchPath);
final bool didPop = route.didPop(result);
if (didPop) replyState.routePath = const ReplyHomePath();
return didPop;
}
@override
Future<void> setNewRoutePath(ReplyRoutePath configuration) {
replyState.routePath = configuration;
return SynchronousFuture<void>(null);
}
}
Esse é o navegador raiz, que processa as partes do app que ocupam a tela inteira, como HomePage
e SearchPage
. Ele detecta o estado do app para conferir se a rota foi definida como ReplySearchPath
. Em caso positivo, ele recriará o navegador com a tela SearchPage
na parte superior da pilha. Observe que nossas telas são envolvidas em uma CustomTransitionPage
sem transições definidas. Isso demonstra uma forma de navegar entre as telas sem transições personalizadas.
home.dart
Para definir nossa rota como ReplySearchPath
no estado do app, fazemos o seguinte dentro de _BottomAppBarActionItems
no home.dart
:
Align(
alignment: AlignmentDirectional.bottomEnd,
child: IconButton(
icon: const Icon(Icons.search),
color: ReplyColors.white50,
onPressed: () {
Provider.of<RouterProvider>(
context,
listen: false,
).routePath = const ReplySearchPath();
},
),
);
No parâmetro onPressed
, acessamos nosso RouterProvider
e definimos o routePath
como ReplySearchPath
. Nosso RouterProvider
monitora o estado dos nossos navegadores raiz.
mail_view_router.dart
Agora, vamos ver como a navegação interna do app está configurada. Abra o mail_view_router.dart
no diretório lib
. Você verá um navegador semelhante ao de cima:
class MailViewRouterDelegate extends RouterDelegate<void>
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
MailViewRouterDelegate({required this.drawerController});
final AnimationController drawerController;
@override
Widget build(BuildContext context) {
bool _handlePopPage(Route<dynamic> route, dynamic result) {
return false;
}
return Selector<EmailStore, String>(
selector: (context, emailStore) => emailStore.currentlySelectedInbox,
builder: (context, currentlySelectedInbox, child) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Fade through transition between mailbox pages (Motion)
CustomTransitionPage(
transitionKey: ValueKey(currentlySelectedInbox),
screen: InboxPage(
destination: currentlySelectedInbox,
),
)
],
);
},
);
}
...
}
Esse é nosso navegador interno. Ele processa as imagens internas do app que consomem apenas o corpo da tela, como a InboxPage
. A InboxPage
exibe uma lista de e-mails que depende de qual caixa de e-mails está no estado do app. O navegador é recriado com a InboxPage
correta no topo da pilha sempre que há mudança na propriedade currentlySelectedInbox
do estado do app.
home.dart
Para definir nossa caixa de e-mails atual no estado do app, fazemos o seguinte dentro do _HomePageState
no home.dart
:
void _onDestinationSelected(String destination) {
var emailStore = Provider.of<EmailStore>(
context,
listen: false,
);
if (emailStore.onMailView) {
emailStore.currentlySelectedEmailId = -1;
}
if (emailStore.currentlySelectedInbox != destination) {
emailStore.currentlySelectedInbox = destination;
}
setState(() {});
}
Na função _onDestinationSelected
, acessamos o EmailStore
e definimos a currentlySelectedInbox
como o destino selecionado. Nosso EmailStore
monitora o estado dos nossos navegadores internos.
home.dart
Por fim, para ver um exemplo de rota de navegação em uso, abra o home.dart
no diretório lib
. Localize a classe _ReplyFabState
dentro da propriedade onTap
do widget InkWell
, que terá esta aparência:
onTap: () {
Provider.of<EmailStore>(
context,
listen: false,
).onCompose = true;
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return const ComposePage();
},
),
);
},
Isso mostra como você pode navegar para a página "Escrever e-mail" sem nenhuma transição personalizada. Durante este codelab, você conhecerá o código do Reply para configurar transições do Material Design que funcionam em conjunto com as várias ações de navegação do app.
Agora que você já conhece o código inicial, vamos implementar nossa primeira transição.
5. Adicionar uma transição de transformação de contêiner da lista de e-mails para a página de detalhes
Para começar, adicione uma transição ao clicar em um e-mail. Para essa mudança de navegação, o padrão de transformação de contêiner é adequado, já que foi projetado para transições entre elementos da IU que incluem um contêiner. Ele cria uma conexão visível entre dois elementos da IU.
Antes de adicionar qualquer código, execute o app Reply e clique em um e-mail. Ele fará um simples corte, ou seja, a tela será substituída sem transição:
Antes
Para começar, adicione uma importação ao pacote de animações na parte superior do mail_card_preview.dart
, como mostrado no snippet a seguir:
mail_card_preview.dart
import 'package:animations/animations.dart';
Agora que você tem uma importação para o pacote de animações, podemos começar a adicionar transições bonitas ao app. Primeiro, crie uma classe StatelessWidget
para hospedar nosso widget OpenContainer
.
No mail_card_preview.dart
, adicione o seguinte snippet de código após a definição de classe do MailPreviewCard
:
mail_card_preview.dart
// TODO: Add Container Transform transition from email list to email detail page (Motion)
class _OpenContainerWrapper extends StatelessWidget {
const _OpenContainerWrapper({
required this.id,
required this.email,
required this.closedChild,
});
final int id;
final Email email;
final Widget closedChild;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return OpenContainer(
openBuilder: (context, closedContainer) {
return MailViewPage(id: id, email: email);
},
openColor: theme.cardColor,
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(0)),
),
closedElevation: 0,
closedColor: theme.cardColor,
closedBuilder: (context, openContainer) {
return InkWell(
onTap: () {
Provider.of<EmailStore>(
context,
listen: false,
).currentlySelectedEmailId = id;
openContainer();
},
child: closedChild,
);
},
);
}
}
Agora, vamos usar o novo wrapper. Dentro da definição da classe MailPreviewCard
, envolveremos o widget Material
da função build()
com o novo _OpenContainerWrapper
:
mail_card_preview.dart
// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
id: id,
email: email,
closedChild: Material(
...
_OpenContainerWrapper
tem um widget do InkWell
e as propriedades de cor do OpenContainer
definem a cor do contêiner. Assim, podemos remover os widgets Material e Inkwell. O código resultante será parecido com este:
mail_card_preview.dart
// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
id: id,
email: email,
closedChild: Dismissible(
key: ObjectKey(email),
dismissThresholds: const {
DismissDirection.startToEnd: 0.8,
DismissDirection.endToStart: 0.4,
},
onDismissed: (direction) {
switch (direction) {
case DismissDirection.endToStart:
if (onStarredInbox) {
onStar();
}
break;
case DismissDirection.startToEnd:
onDelete();
break;
default:
}
},
background: _DismissibleContainer(
icon: 'twotone_delete',
backgroundColor: colorScheme.primary,
iconColor: ReplyColors.blue50,
alignment: Alignment.centerLeft,
padding: const EdgeInsetsDirectional.only(start: 20),
),
confirmDismiss: (direction) async {
if (direction == DismissDirection.endToStart) {
if (onStarredInbox) {
return true;
}
onStar();
return false;
} else {
return true;
}
},
secondaryBackground: _DismissibleContainer(
icon: 'twotone_star',
backgroundColor: currentEmailStarred
? colorScheme.secondary
: theme.scaffoldBackgroundColor,
iconColor: currentEmailStarred
? colorScheme.onSecondary
: colorScheme.onBackground,
alignment: Alignment.centerRight,
padding: const EdgeInsetsDirectional.only(end: 20),
),
child: mailPreview,
),
);
Nesta fase, você já tem uma transformação de contêiner em funcionamento. Se você clicar em um e-mail, o item da lista será expandido para uma tela de detalhes enquanto a lista de e-mails for ocultada. Se você pressionar "Voltar", a tela de detalhes do e-mail será recolhida e a lista de e-mails voltará a aparecer.
Depois
6. Adicionar uma transição de transformação de contêiner do FAB para a página "Escrever e-mail"
Vamos continuar a transformação de contêiner e adicionar uma transição do botão de ação flutuante para a ComposePage
, expandindo o FAB para um novo e-mail que será escrito pelo usuário. Primeiro, execute o app novamente e clique no FAB. Você verá que não há transição ao abrir a tela "Escrever e-mail".
Antes
Vamos configurar essa transição de maneira muito semelhante ao que fizemos na etapa anterior, já que estamos usando a mesma classe de widget, a OpenContainer
.
Em home.dart
, vamos importar package:animations/animations.dart
na parte de cima do arquivo e modificar o método _ReplyFabState
build()
. Vamos combinar o widget Material
com o widget OpenContainer
:
home.dart
// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
openBuilder: (context, closedContainer) {
return const ComposePage();
},
openColor: theme.cardColor,
onClosed: (success) {
Provider.of<EmailStore>(
context,
listen: false,
).onCompose = false;
},
closedShape: circleFabBorder,
closedColor: theme.colorScheme.secondary,
closedElevation: 6,
closedBuilder: (context, openContainer) {
return Material(
color: theme.colorScheme.secondary,
...
Além dos parâmetros usados para configurar o widget OpenContainer
anterior, o onClosed
também será definido. O onClosed
é um ClosedCallback
chamado quando a rota OpenContainer
é exibida ou quando ela retorna ao estado fechado. O valor de retorno dessa transação é transmitido à função como um argumento. Usamos esse Callback
para notificar o provedor do app sobre a saída da rota ComposePage
. Assim, ele pode notificar todos os listeners.
Assim como fizemos na etapa anterior, removeremos o widget Material
do nosso widget, já que o OpenContainer
processa a cor do widget retornado pelo closedBuilder
com closedColor
. Removeremos também a chamada Navigator.push()
dentro do onTap
do widget InkWell, substituindo-a pelo openContainer() Callback
fornecido pelo closedBuilder
do widget OpenContainer
, já que agora o OpenContainer
processa a própria rota.
O código resultante será parecido com este:
home.dart
// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
openBuilder: (context, closedContainer) {
return const ComposePage();
},
openColor: theme.cardColor,
onClosed: (success) {
Provider.of<EmailStore>(
context,
listen: false,
).onCompose = false;
},
closedShape: circleFabBorder,
closedColor: theme.colorScheme.secondary,
closedElevation: 6,
closedBuilder: (context, openContainer) {
return Tooltip(
message: tooltip,
child: InkWell(
customBorder: circleFabBorder,
onTap: () {
Provider.of<EmailStore>(
context,
listen: false,
).onCompose = true;
openContainer();
},
child: SizedBox(
height: _mobileFabDimension,
width: _mobileFabDimension,
child: Center(
child: fabSwitcher,
),
),
),
);
},
);
Agora, vamos limpar parte do código antigo. Como o widget OpenContainer
agora processa a notificação do provedor do app sobre a saída da ComposePage
pelo onClosed ClosedCallback
, podemos remover a implementação anterior no mail_view_router.dart
:
mail_view_router.dart
// TODO: Add Container Transform from FAB to compose email page (Motion)
emailStore.onCompose = false; /// delete this line
return SynchronousFuture<bool>(true);
Pronto, concluímos esta etapa. Agora você tem uma transição do FAB para a tela "Escrever e-mail" com a seguinte aparência:
Depois
7. Adicionar uma transição de eixo z compartilhado do ícone de pesquisa para a página de visualização de pesquisa
Nesta etapa, adicionaremos uma transição do ícone de pesquisa para a visualização de pesquisa em tela cheia. Como não há um contêiner persistente envolvido nessa mudança de navegação, podemos usar uma transição de eixo z compartilhado para reforçar a relação espacial entre as duas telas e indicar o movimento para um nível acima na hierarquia do app.
Antes de adicionar mais código, execute o app e toque no ícone de pesquisa no canto inferior direito da tela. Essa ação exibirá a tela de visualização da pesquisa sem transição.
Antes
Para começar, acesse o arquivo router.dart
. Depois que definir a classe ReplySearchPath
, adicione o seguinte snippet:
router.dart
// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
class SharedAxisTransitionPageWrapper extends Page {
const SharedAxisTransitionPageWrapper(
{required this.screen, required this.transitionKey})
: super(key: transitionKey);
final Widget screen;
final ValueKey transitionKey;
@override
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SharedAxisTransition(
fillColor: Theme.of(context).cardColor,
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.scaled,
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) {
return screen;
});
}
}
Agora, usaremos o novo SharedAxisTransitionPageWrapper
para criar a transição desejada. Dentro da definição da classe ReplyRouterDelegate
na propriedade pages
, vamos combinar a tela de pesquisa com um SharedAxisTransitionPageWrapper
em vez de CustomTransitionPage
:
router.dart
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
const CustomTransitionPage(
transitionKey: ValueKey('Home'),
screen: HomePage(),
),
if (routePath is ReplySearchPath)
const SharedAxisTransitionPageWrapper(
transitionKey: ValueKey('Search'),
screen: SearchPage(),
),
],
);
Agora, execute o app novamente.
Tudo está ficando ótimo. Quando você clica no ícone de pesquisa na barra de apps inferior, uma transição de eixo compartilhado redimensiona a página de pesquisa para que ela seja exibida. No entanto, observe como a página inicial não é redimensionada, permanecendo estática à medida que a página de pesquisa aumenta sobre ela. Além disso, quando o botão "Voltar" é pressionado, a página inicial não é redimensionada para a visualização: ela fica parada enquanto a página de pesquisa é recolhida. Ou seja, o trabalho ainda não terminou.
Vamos resolver os dois problemas ao combinar HomePage
com nosso SharedAxisTransitionWrapper
em vez de CustomTransitionPage
:
router.dart
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
const SharedAxisTransitionPageWrapper(
transitionKey: ValueKey('home'),
screen: HomePage(),
),
if (routePath is ReplySearchPath)
const SharedAxisTransitionPageWrapper(
transitionKey: ValueKey('search'),
screen: SearchPage(),
),
],
);
Pronto! Agora, execute novamente o app e toque no ícone de pesquisa. As telas inicial e de visualização da pesquisa serão esmaecidas e redimensionadas simultaneamente ao longo do eixo z em profundidade, criando um efeito contínuo entre as duas telas.
Depois
8. Adicionar uma transição de esmaecimento cruzado entre as páginas da caixa de e-mails
Nesta etapa, adicionaremos uma transição entre caixas de e-mails diferentes. Como não queremos enfatizar uma relação espacial ou hierárquica, usaremos um esmaecimento cruzado para fazer uma "troca" simples entre as listas de e-mails.
Antes de adicionar qualquer outro código, tente executar o app. Toque no logotipo do Reply na barra de apps inferior e mude de caixa de e-mails. A lista mudará sem nenhuma transição.
Antes
Para começar, acesse o arquivo mail_view_router.dart
. Depois que definir a classe MailViewRouterDelegate
, adicione o seguinte snippet:
mail_view_router.dart
// TODO: Add Fade through transition between mailbox pages (Motion)
class FadeThroughTransitionPageWrapper extends Page {
const FadeThroughTransitionPageWrapper({
required this.mailbox,
required this.transitionKey,
}) : super(key: transitionKey);
final Widget mailbox;
final ValueKey transitionKey;
@override
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
fillColor: Theme.of(context).scaffoldBackgroundColor,
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) {
return mailbox;
});
}
}
Assim como fizemos na etapa anterior, vamos usar o novo FadeThroughTransitionPageWrapper
para criar a transição desejada. Dentro da definição da classe MailViewRouterDelegate
, na propriedade pages
, em vez de combinar nossa tela de caixa de e-mails com um CustomTransitionPage
, use FadeThroughTransitionPageWrapper
:
mail_view_router.dart
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Fade through transition between mailbox pages (Motion)
FadeThroughTransitionPageWrapper(
mailbox: InboxPage(destination: currentlySelectedInbox),
transitionKey: ValueKey(currentlySelectedInbox),
),
],
);
Execute o app novamente. Quando você abrir a gaveta de navegação inferior e mudar de caixa de e-mails, a lista de e-mails atual será esmaecida enquanto a nova lista é exibida. Muito bem!
Depois
9. Adicionar uma transição de esmaecimento cruzado entre o FAB de escrever e o de responder
Nesta etapa, adicionaremos uma transição entre diferentes ícones do FAB. Como não queremos enfatizar uma relação espacial ou hierárquica, usaremos um esmaecimento cruzado para fazer uma "troca" simples entre os ícones do FAB.
Antes de adicionar qualquer outro código, tente executar o app. Toque em um e-mail e abra a visualização de e-mail. O ícone do FAB mudará sem uma transição.
Antes
Trabalharemos no home.dart
no restante do codelab. Portanto, não se preocupe em adicionar a importação do pacote de animações, porque já fizemos isso no home.dart
na etapa 2.
Vamos configurar as próximas duas transições de maneira muito semelhante, já que todas elas criarão com base em uma classe reutilizável, _FadeThroughTransitionSwitcher
.
No home.dart
, adicione o seguinte snippet no _ReplyFabState
:
home.dart
// TODO: Add Fade through transition between compose and reply FAB (Motion)
class _FadeThroughTransitionSwitcher extends StatelessWidget {
const _FadeThroughTransitionSwitcher({
required this.fillColor,
required this.child,
});
final Widget child;
final Color fillColor;
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher(
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
fillColor: fillColor,
child: child,
animation: animation,
secondaryAnimation: secondaryAnimation,
);
},
child: child,
);
}
}
Agora, no _ReplyFabState
, procure o widget fabSwitcher
. fabSwitcher
retorna um ícone diferente dependendo da visualização do e-mail (se estiver ativada ou não). Vamos fazer a combinação com _FadeThroughTransitionSwitcher
:
home.dart
// TODO: Add Fade through transition between compose and reply FAB (Motion)
static final fabKey = UniqueKey();
static const double _mobileFabDimension = 56;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final circleFabBorder = const CircleBorder();
return Selector<EmailStore, bool>(
selector: (context, emailStore) => emailStore.onMailView,
builder: (context, onMailView, child) {
// TODO: Add Fade through transition between compose and reply FAB (Motion)
final fabSwitcher = _FadeThroughTransitionSwitcher(
fillColor: Colors.transparent,
child: onMailView
? Icon(
Icons.reply_all,
key: fabKey,
color: Colors.black,
)
: const Icon(
Icons.create,
color: Colors.black,
),
);
...
Fornecemos ao _FadeThroughTransitionSwitcher
um fillColor
transparente para que não haja um plano de fundo entre os elementos durante a transição. Também criamos o UniqueKey
e o atribuímos a um dos ícones.
Agora, nesta etapa, você terá um FAB contextual animado. Quando você entra em uma visualização de e-mail, o ícone do FAB antigo é ocultado enquanto o novo é exibido.
Depois
10. Adicionar uma transição de esmaecimento cruzado entre títulos da caixa de e-mails que desaparecem
Nesta etapa, adicionaremos uma transição de esmaecimento cruzado para mudar o título da caixa de e-mails entre o estado visível e o invisível durante a visualização de um e-mail. Como não queremos enfatizar uma relação espacial ou hierárquica, usaremos um esmaecimento cruzado para fazer uma "troca" simples entre o widget Text
, que inclui o título da caixa de e-mails, e um SizedBox
vazio.
Antes de adicionar qualquer outro código, tente executar o app. Toque em um e-mail e abra a visualização de e-mail. O título da caixa de e-mails desaparecerá sem uma transição.
Antes
O restante deste codelab será rápido, já que fizemos a maior parte do trabalho no _FadeThroughTransitionSwitcher
na etapa anterior.
Agora, vamos acessar a classe _AnimatedBottomAppBar
no home.dart
para adicionar a transição. Reutilizaremos o _FadeThroughTransitionSwitcher
da última etapa e envolveremos nossa condicional onMailView
, que retorna um SizedBox
vazio ou um título da caixa de e-mails que é esmaecido em sincronia com a gaveta inferior:
home.dart
...
const _ReplyLogo(),
const SizedBox(width: 10),
// TODO: Add Fade through transition between disappearing mailbox title (Motion)
_FadeThroughTransitionSwitcher(
fillColor: Colors.transparent,
child: onMailView
? const SizedBox(width: 48)
: FadeTransition(
opacity: fadeOut,
child: Selector<EmailStore, String>(
selector: (context, emailStore) =>
emailStore.currentlySelectedInbox,
builder: (
context,
currentlySelectedInbox,
child,
) {
return Text(
currentlySelectedInbox,
style: Theme.of(context)
.textTheme
.bodyText1!
.copyWith(
color: ReplyColors.white50,
),
);
},
),
),
),
Pronto. Você concluiu esta etapa.
Execute o app novamente. Ao abrir um e-mail e acessar a visualização dele, o título da caixa de e-mails na barra de apps inferior será esmaecido e ocultado. Incrível!
Depois
11. Adicionar uma transição de esmaecimento cruzado entre as ações da barra de apps inferior
Nesta etapa, adicionaremos uma transição de esmaecimento cruzado para esmaecer as ações da barra de apps inferior com base no contexto do aplicativo. Como não queremos enfatizar uma relação espacial ou hierárquica, usaremos um esmaecimento cruzado para fazer uma "troca" simples entre as ações da barra de apps inferior quando o app está na HomePage, quando a gaveta inferior está visível e quando estamos na visualização de e-mail.
Antes de adicionar qualquer outro código, tente executar o app. Toque em um e-mail e abra a visualização de e-mail. Você também pode tocar no logotipo do Reply. As ações da barra de apps inferior mudarão sem uma transição.
Antes
Assim como na etapa anterior, usaremos o _FadeThroughTransitionSwitcher
novamente. Para conseguir a transição desejada, acesse a definição da classe _BottomAppBarActionItems
e envolva o widget de retorno da função build()
com um _FadeThroughTransitionSwitcher
:
home.dart
// TODO: Add Fade through transition between bottom app bar actions (Motion)
return _FadeThroughTransitionSwitcher(
fillColor: Colors.transparent,
child: drawerVisible
? Align(
key: UniqueKey(),
alignment: AlignmentDirectional.bottomEnd,
child: IconButton(
icon: const Icon(Icons.settings),
color: ReplyColors.white50,
onPressed: () async {
drawerController.reverse();
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: modalBorder,
),
builder: (context) => const SettingsBottomSheet(),
);
},
),
)
: onMailView
...
Vamos fazer um teste. Ao abrir um e-mail e acessar a visualização dele, as ações anteriores da barra de apps inferior serão esmaecidas e ocultadas e as novas ações serão exibidas. Muito bem!
Depois
12. Parabéns!
Usando menos de 100 linhas de código Dart, o pacote de animações ajudou você a criar transições bonitas em um app atual seguindo as diretrizes do Material Design. Além disso, a aparência e o comportamento das transições são consistentes em todos os dispositivos.
Próximas etapas
Para ver mais informações sobre o sistema de movimento do Material Design, consulte as especificações e a documentação completa do desenvolvedor (links em inglês). Tente adicionar algumas transições do Material Design ao seu app.
Agradecemos por testar o movimento do Material Design. Esperamos que tenha gostado deste codelab.
Este codelab exigiu esforço e tempo normais para ser concluído
Quero continuar usando o sistema de movimento do Material Design no futuro
Confira a Galeria do Flutter
Para conferir mais demonstrações de como usar os widgets fornecidos pela biblioteca Flutter do Material Design, além do framework do Flutter, acesse a Galeria do Flutter. |