1. Introducción
Material Design es un sistema para crear productos digitales atractivos y llamativos. Mediante la unión de estilo, desarrollo de la marca, interacción y movimiento en un conjunto coherente de principios y componentes, los equipos de productos pueden alcanzar su máximo potencial de diseño.
Los componentes de Material (MDC) ayudan a los desarrolladores a implementar Material Design. Los MDC, creados por un equipo de ingenieros y diseñadores de UX en Google, cuentan con decenas de componentes de IU atractivos y funcionales, y están disponibles para Android, iOS, la Web y Flutter.material.io/develop. |
¿Qué es el sistema de movimiento de Material para Flutter?
El sistema de movimiento de Material para Flutter es un conjunto de patrones de transición dentro del paquete de animaciones que ayuda a los usuarios a comprender y navegar por una app, como se describe en los lineamientos de Material Design.
Estos son los cuatro patrones de transición principales de Material:
- Transformación de contenedores: genera una transición entre elementos de la IU que incluyen un contenedor; crea una conexión visible entre dos elementos de la IU diferentes mediante una transformación fluida de un elemento en otro.
- Eje compartido: genera una transición entre elementos de la IU que tienen una relación espacial o de navegación; utiliza una transformación compartida en los ejes X, Y o Z para reforzar la relación entre elementos.
- Atenuación rápida: genera una transición entre elementos de la IU que no tienen una relación estrecha entre sí; utiliza una atenuación secuencial de entrada y salida, con la escala de un elemento nuevo.
- Atenuación: se utiliza para los elementos de la IU que entran a los límites de la pantalla o salen de estos.
El paquete de animaciones ofrece widgets de transición para estos patrones, que se compilaron sobre la biblioteca de animaciones de Flutter (flutter/animation.dart
) y la biblioteca de Material de Flutter (flutter/material.dart
):
En este codelab, usarás las transiciones de Material que se compilaron sobre el framework de Flutter y la biblioteca de Material, lo que implica que trabajarás con widgets. :)
Qué compilarás
En este codelab, usarás Dart para compilar algunas transiciones en una app de ejemplo de correo electrónico de Flutter que se llama Reply. Te guiaremos para que comprendas cómo usar las transiciones del paquete de animaciones con el objeto de personalizar el aspecto de tu app.
Se te brindará el código de inicio para la app de Reply, e incorporarás en ella las transiciones de Material que se pueden observar en el siguiente GIF del codelab completo:
- Transición de transformación de contenedores desde la lista de direcciones de correo electrónico hasta la página de detalles del correo electrónico
- Transición de transformación de contenedores desde el BAF hasta la página para redactar correos electrónicos
- Transición de eje Z compartido desde el ícono de búsqueda hasta la página de la vista de búsqueda
- Transición de fundido entre las páginas de los buzones
- Transición de fundido entre el BAF para redactar y para responder
- Transición de fundido entre el título del buzón que desaparece
- Transición de atenuación entre las acciones de la barra inferior de la aplicación
Requisitos
- Conocimientos básicos sobre el desarrollo de Flutter y Dart
- Un editor de código
- Un emulador o dispositivo con Android o iOS
- El código de muestra (consulta el siguiente paso)
¿Cómo calificarías tu nivel de experiencia con la compilación de apps de Flutter?
¿Qué te gustaría aprender en este codelab?
2. Configura tu entorno de desarrollo de Flutter
Para completar este lab, necesitas dos programas de software: el SDK de Flutter y un editor.
Puedes ejecutar el codelab con cualquiera de estos dispositivos o modalidades:
- Un dispositivo físico Android o iOS conectado a tu computadora y configurado en el Modo de desarrollador
- El simulador de iOS (requiere instalar las herramientas de Xcode)
- Android Emulator (requiere configuración en Android Studio)
- Un navegador (se requiere Chrome para la depuración)
- Como una aplicación para computadoras que ejecuten Windows, Linux o macOS (debes desarrollarla en la plataforma donde tengas pensado realizar la implementación; por lo tanto, si quieres desarrollar una app de escritorio para Windows, debes desarrollarla en ese SO a fin de obtener acceso a la cadena de compilación correcta; encuentra detalles sobre los requisitos específicos del sistema operativo en docs.flutter.dev/desktop).
3. Descarga la app de partida del codelab
Opción 1: Clona la app de partida del codelab desde GitHub
Para clonar este codelab desde GitHub, ejecuta los siguientes comandos:
git clone https://github.com/material-components/material-components-flutter-motion-codelab.git cd material-components-flutter-motion-codelab
Opción 2: Descarga el archivo ZIP de la app de inicio del codelab
La app de partida se encuentra en el directorio material-components-flutter-motion-codelab-starter
.
Verifica las dependencias del proyecto
El proyecto depende del paquete de animaciones. En pubspec.yaml
, observa que la sección dependencies
incluye lo siguiente:
animations: ^2.0.0
Abre el proyecto y ejecuta la app
- Abre el proyecto en el editor que prefieras.
- Sigue las instrucciones para “ejecutar la app” en Get Started: Test drive en el editor que elegiste.
Listo. El código de partida para la página principal de Reply se debe estar ejecutando en tu emulador o dispositivo. Deberías ver la carpeta Recibidos con una lista de correos electrónicos.
Disminuye las animaciones del dispositivo (opcional)
Como este codelab abarca transiciones rápidas y refinadas, puede resultar útil disminuir las animaciones del dispositivo para observar algunos de los detalles más sutiles de las transiciones durante la implementación. Esto se puede establecer mediante una opción de configuración en la app, a la que se puede acceder con el ícono de configuración cuando abres el panel lateral inferior. No te preocupes, este método para disminuir las animaciones del dispositivo no afectará las animaciones en el dispositivo fuera de la app de Reply.
Modo oscuro (opcional)
Si el tema brillante de Reply te causa molestias en los ojos, tenemos una solución. En la app, se incluye una configuración que te permite cambiar el tema al modo oscuro para que se adapte mejor a la sensibilidad de los ojos. A fin de acceder a este parámetro de configuración, presiona el ícono de configuración cuando abres el panel lateral inferior.
4. Familiarízate con el código de la app de ejemplo
Echemos un vistazo al código. Brindamos una app que usa el paquete de animaciones para hacer la transición entre diferentes pantallas de la aplicación.
- HomePage: muestra el buzón seleccionado.
- InboxPage: muestra una lista de correos electrónicos.
- MailPreviewCard: muestra la vista previa de un correo electrónico.
- MailViewPage: muestra un solo correo electrónico completo.
- ComposePage: permite redactar un correo electrónico nuevo.
- SearchPage: muestra una vista de búsqueda.
router.dart
Primero, para comprender cómo se configura la navegación raíz de la app, abre router.dart
en el directorio 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);
}
}
Este es el navegador raíz, que controla las pantallas de nuestra app que abarcan el lienzo completo, como HomePage
y SearchPage
. Además, detecta el estado de nuestra app para verificar si configuramos la ruta a ReplySearchPath
. Si es así, vuelve a compilar el navegador con la SearchPage
en la parte superior de la pila. Observa que las pantallas están unidas a un elemento CustomTransitionPage
sin transiciones definidas. De esta manera, te mostramos una manera de navegar entre las pantallas sin ninguna transición personalizada.
home.dart
Para configurar la ruta en ReplySearchPath
en el estado de nuestra app, haz lo siguiente dentro de _BottomAppBarActionItems
en 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();
},
),
);
En el parámetro onPressed
, accedemos al elemento RouterProvider
y configuramos su objeto routePath
como ReplySearchPath
. El elemento RouterProvider
realiza un seguimiento del estado de los navegadores raíz.
mail_view_router.dart
Ahora, analicemos cómo está configurada la navegación interna de nuestra app. Abre mail_view_router.dart
en el directorio lib
. Verás un navegador similar al que se mostró anteriormente:
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,
),
)
],
);
},
);
}
...
}
Este es nuestro navegador interno. Controla las pantallas internas de nuestra app que abarcan solo el cuerpo del lienzo, como la InboxPage
. El elemento InboxPage
muestra una lista de correos electrónicos en función del buzón actual en el estado de la app. El navegador se vuelve a compilar con el elemento InboxPage
correcto en la parte superior de la pila, siempre que haya un cambio en la propiedad currentlySelectedInbox
del estado de nuestra app.
home.dart
Para configurar el buzón actual en el estado de nuestra app, haz lo siguiente dentro de _HomePageState
en 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(() {});
}
En la función _onDestinationSelected
, accedemos al elemento EmailStore
y configuramos su objeto currentlySelectedInbox
en el destino seleccionado. El elemento EmailStore
realiza un seguimiento del estado de los navegadores internos.
home.dart
Por último, para ver un ejemplo de un enrutamiento de navegación en uso, abre home.dart
en el directorio lib
. Ubica la clase _ReplyFabState
, dentro de la propiedad onTap
del widget InkWell
, que debería verse de la siguiente manera:
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();
},
),
);
},
Esto demuestra cómo navegar a la página para redactar correos electrónicos, sin ninguna transición personalizada. Durante este codelab, profundizarás en el código de Reply para configurar transiciones de Material que funcionen en conjunto con las varias acciones de navegación en toda la app.
Ahora que te familiarizaste con el código de partida, implementemos la primera transición.
5. Agrega una transición de transformación de contenedores desde la lista de direcciones de correo electrónico hasta la página de detalles del correo electrónico
En principio, agregarás una transición para la acción de hacer clic en un correo electrónico. Para este cambio de navegación, se prefiere el patrón de transformación de contenedores, ya que está diseñado para hacer transiciones entre elementos de la IU que incluyen un contenedor. Este patrón crea una conexión visible entre dos elementos de la IU.
Antes de agregar un fragmento de código, prueba ejecutar la app de Reply y hacer clic en un correo electrónico. Debería observarse un corte abrupto y simple, lo que implica que la pantalla se reemplaza por una sin transición:
Antes
Comienza agregando una importación para el paquete de animaciones en la parte superior de mail_card_preview.dart
, como se muestra en el siguiente fragmento:
mail_card_preview.dart
import 'package:animations/animations.dart';
Ahora que tienes una importación para el paquete de animaciones, podemos comenzar a agregar transiciones atractivas a tu app. Comencemos creando una clase StatelessWidget
que alojará el widget OpenContainer
.
En mail_card_preview.dart
, agrega el siguiente fragmento de código después de la definición de la clase 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,
);
},
);
}
}
Ahora, usemos el wrapper nuevo. Dentro de la definición de la clase MailPreviewCard
, uniremos el widget Material
desde la función build()
con el nuevo elemento _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
tiene un widget de InkWell
y las propiedades de color de OpenContainer
definen el color del contenedor que encierra. Por lo tanto, podemos quitar los widgets de InkWell y Material. El código resultante se ve así:
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,
),
);
En esta etapa, la transformación de contenedores debe funcionar por completo. Cuando haces clic en un correo electrónico, el elemento de la lista se expande en una pantalla de detalles y, al mismo tiempo, se desvanece la lista de correos electrónicos. Cuando presionas el botón Atrás, la pantalla de detalles del correo electrónico se contrae en un elemento de la lista mientras se escala verticalmente la lista de correos electrónicos.
Después
6. Agrega una transición de transformación de contenedores desde el BAF hasta la página para redactar correos electrónicos
Continuemos con la transformación de contenedores y agreguemos una transición desde el botón de acción flotante (BAF) hasta ComposePage
expandiendo el BAF a un correo electrónico nuevo que escribirá el usuario. Primero, vuelve a ejecutar la app y haz clic en el BAF a fin de verificar que no se hagan transiciones cuando se inicia la pantalla para redactar correos electrónicos.
Antes
El método de configuración de esta transición será muy similar al que hicimos en el último paso, ya que usamos la misma clase de widget, OpenContainer
.
En home.dart
, importemos package:animations/animations.dart
en la parte superior del archivo y modifiquemos el método de _ReplyFabState
build()
. Unamos el widget de Material
que se muestra con uno de 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,
...
Además de los parámetros que se usaron para configurar el widget OpenContainer
anterior, ahora también se configura onClosed
. onClosed
es un elemento ClosedCallback
al que se llama cuando se resalta la ruta OpenContainer
o cuando vuelve al estado cerrado. El valor que se muestra de esa transacción se pasa a esta función como un argumento. Usamos esta Callback
para notificarle al proveedor de nuestra app que dejamos la ruta de ComposePage
, de modo que pueda notificar a todos los objetos de escucha.
De forma similar a lo que hicimos para el último paso, quitaremos el widget de Material
de nuestro widget, ya que el widget de OpenContainer
controla el color del widget que muestra el closedBuilder
con closedColor
. Además, quitaremos la llamada Navigator.push()
dentro de la acción onTap
del widget InkWell y la reemplazaremos por la openContainer() Callback
que brinda el closedBuilder
del widget OpenContainer
, ya que ahora OpenContainer
controla su propio enrutamiento.
El código resultante quedará así:
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,
),
),
),
);
},
);
Ahora, borra algunos códigos anteriores. Como el widget OpenContainer
ahora controla el proceso para notificarle al proveedor de nuestra app que ya no estamos en la ComposePage
a través de una onClosed ClosedCallback
, podemos quitar la implementación anterior en 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);
Ya terminaste con este paso. Deberías ver una transición desde el BAF hasta la pantalla para redactar de la siguiente manera:
Después
7. Agrega una transición de eje Z compartido desde el ícono de búsqueda hasta la página de la vista de búsqueda
En este paso, agregaremos una transición desde el ícono de búsqueda hasta la vista de búsqueda directa en pantalla completa. Como no existe un contenedor persistente en este cambio de navegación, se puede usar una transición de eje Z compartido para reforzar la relación espacial entre las dos pantallas y para indicar que se mueva un nivel en dirección ascendente en la jerarquía de la app.
Antes de agregar más fragmentos de código, prueba ejecutar la app y presionar el ícono de búsqueda en la esquina inferior derecha de la pantalla. Debería aparecer la pantalla de la vista de búsqueda sin transición.
Antes
Para comenzar, ve al archivo router.dart
. Después de la definición de la clase ReplySearchPath
, agrega el siguiente fragmento:
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;
});
}
}
Ahora, usemos el nuevo SharedAxisTransitionPageWrapper
para obtener la transición que deseamos. Dentro de la definición de la clase ReplyRouterDelegate
, en la propiedad pages
, unamos la búsqueda directa con un SharedAxisTransitionPageWrapper
en lugar de una 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(),
),
],
);
Ahora, prueba volver a ejecutar la app.
Todo comienza a verse increíble. Cuando haces clic en el ícono de búsqueda, en la barra inferior de la app, la transición de eje compartido expande la escala de la página de búsqueda hasta que sea visible. Sin embargo, observa cómo no se reduce la escala de la página principal y, en su lugar, se mantiene estática, a medida que se expande la escala de la página de búsqueda sobre la otra. Además, cuando presionas el botón Atrás, no se expande la escala de la página principal hasta que sea visible; en cambio, se mantiene estática, a medida que se reduce la escala de la página de búsqueda. Esto significa que aún no terminamos.
Corrijamos ambos problemas uniendo la HomePage
con SharedAxisTransitionWrapper
en lugar 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(),
),
],
);
Eso es todo. Ahora, prueba volver a ejecutar la app y presionar el ícono de búsqueda. Las pantallas de inicio y de la vista de búsqueda deben atenuarse y escalar, de manera simultánea, en el eje Z en profundidad. De esta manera, se crea un efecto fluido entre las dos pantallas.
Después
8. Agrega una transición de atenuación entre las páginas de los buzones
En este paso, agregaremos una transición entre diferentes buzones. Como no deseamos enfatizar una relación espacial ni jerárquica, usaremos un fundido para realizar un "intercambio" sencillo entre las listas de correos electrónicos.
Antes de agregar más fragmentos de código, prueba ejecutar la app, presiona el logotipo de Reply en la barra inferior de la aplicación y cambia los buzones. La lista de correos electrónicos debería cambiar sin transición.
Antes
Para comenzar, ve al archivo mail_view_router.dart
. Después de la definición de la clase MailViewRouterDelegate
, agrega el siguiente fragmento:
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;
});
}
}
De manera similar al último paso, usemos el nuevo FadeThroughTransitionPageWrapper
para obtener la transición que deseamos. Dentro de la definición de la clase MailViewRouterDelegate
, en la propiedad pages
, en lugar de unir la pantalla de los buzones con CustomTransitionPage
, usa 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),
),
],
);
Vuelve a ejecutar la app. Cuando abres el panel lateral inferior de navegación y cambias los buzones, se atenúa la lista actual de correos electrónicos y se reduce su escala, mientras que la lista nueva se atenúa y su escala se expande. ¡Genial!
Después
9. Agrega una transición de atenuación entre el BAF para redactar y para responder
En este paso, agregaremos una transición entre diferentes íconos del BAF. Como no deseamos enfatizar una relación espacial ni jerárquica, usaremos un fundido para realizar un "intercambio" sencillo entre los íconos en el BAF.
Antes de agregar más fragmentos de código, prueba ejecutar la app, presionar un correo electrónico y abrir la vista de correo electrónico. El ícono del BAF debería cambiar sin una transición.
Antes
Trabajaremos en home.dart
para el resto del codelab, así que no te preocupes por agregar la importación para el paquete de animaciones, ya que lo hicimos para home.dart
en el paso 2.
La manera en que configuramos las siguientes dos transiciones será muy similar, ya que todas nos brindarán una clase _FadeThroughTransitionSwitcher
reutilizable.
En home.dart
, agregaremos el siguiente fragmento en _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,
);
}
}
Ahora, en _ReplyFabState
, busca el widget fabSwitcher
. fabSwitcher
muestra un ícono diferente en función de si es una vista de correo electrónico o no. Unámoslo con _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,
),
);
...
Le brindamos al elemento _FadeThroughTransitionSwitcher
un fillColor
transparente, de modo que no se muestre ningún fondo entre los elementos durante la transición. También creamos una UniqueKey
y la asignamos a uno de los íconos.
Ahora, en este paso, deberías observar un BAF contextual con una animación completa. Cuando se abre una vista de correo electrónico, se atenúa el ícono anterior del BAF y se reduce su escala, mientras que la vista nueva se atenúa y su escala se expande.
Después
10. Agrega una transición de atenuación entre el título del buzón que desaparece
En este paso, agregaremos una transición de atenuación para el título del buzón entre un estado visible e invisible en una vista de correo electrónico. Como no deseamos enfatizar una relación espacial ni jerárquica, usaremos un fundido para realizar un "intercambio" sencillo entre el widget Text
que abarca el título del buzón y un elemento SizedBox
vacío.
Antes de agregar más fragmentos de código, prueba ejecutar la app, presionar un correo electrónico y abrir la vista de correo electrónico. El título del buzón debería desaparecer sin una transición.
Antes
El resto de este codelab será rápido, porque, para el último paso, ya hicimos la mayor parte del trabajo en _FadeThroughTransitionSwitcher
.
Ahora, vayamos a la clase _AnimatedBottomAppBar
en home.dart
para agregar la transición. Volveremos a utilizar _FadeThroughTransitionSwitcher
del último paso y uniremos el condicional onMailView
, que muestra un objeto SizedBox
vacío o un título de buzón que se atenúa en sincronización con el panel lateral 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,
),
);
},
),
),
),
Eso es todo. Terminamos este paso.
Vuelve a ejecutar la app. Cuando abras un correo electrónico y te dirijas a la vista de correo electrónico, se debería atenuar el título del buzón en la barra inferior de la aplicación, y debería reducirse su escala. ¡Genial!
Después
11. Agrega una transición de atenuación entre las acciones de la barra inferior de la aplicación
En este paso, agregaremos una transición de atenuación para las acciones de la barra inferior de la aplicación según el contexto de las aplicaciones. Como no deseamos enfatizar una relación espacial ni jerárquica, usaremos un fundido para realizar un "intercambio" sencillo entre las acciones de la barra inferior de la aplicación cuando la app esté en la página inicial, cuando el panel lateral inferior esté visible y cuando estemos en la vista de correo electrónico.
Antes de agregar más fragmentos de código, prueba ejecutar la app, presionar un correo electrónico y abrir la vista de correo electrónico. También, intenta presionar el logotipo de Reply. Las acciones de la barra inferior de la app deben cambiar sin una transición.
Antes
De manera similar al último paso, volveremos a usar _FadeThroughTransitionSwitcher
. Para lograr la transición deseada, ve a la definición de la clase _BottomAppBarActionItems
y une el widget que se muestra de la función build()
con _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
...
Ahora, probémoslo. Cuando abras un correo electrónico y te dirijas a la vista de correo electrónico, se atenuarán las acciones anteriores de la barra inferior de la aplicación y se reducirán sus escalas, mientras que se atenuarán las acciones nuevas y sus escalas se expandirán. ¡Bien hecho!
Después
12. ¡Felicitaciones!
Con menos de 100 líneas de código en Dart, el paquete de animaciones te permitió crear transiciones atractivas en una app existente que cumple con los lineamientos de Material Design y también tiene un aspecto y comportamiento coherentes en todos los dispositivos.
Próximos pasos
Si deseas obtener más información sobre el sistema de movimiento de Material, asegúrate de consultar la especificación y la documentación completa para desarrolladores, y prueba agregar algunas transiciones de Material a tu app.
Gracias por probar el sistema de movimiento de Material. Esperamos que hayas disfrutado de este codelab.
Pude completar este codelab en una cantidad de tiempo y con un nivel de esfuerzo razonables
Me gustaría seguir usando el sistema de movimiento de Material en el futuro.
Consulta la galería de Flutter
Para ver más demostraciones sobre cómo usar los widgets que brinda la biblioteca de Material de Flutter y el framework de Flutter, asegúrate de visitar la galería de Flutter. |