1. Introducción
Los componentes de Material (MDC) ayudan a los desarrolladores a implementar Material Design. MDC, creado por un equipo de ingenieros y diseñadores de UX en Google, cuenta con decenas de componentes de IU atractivos y funcionales, y está disponible para Android, iOS, la Web y Flutter.material.io/develop. |
En el codelab MDC-103, personalizaste el color, la elevación, la tipografía y la forma de los componentes de Material (MDC) para crear el estilo de tu app.
Los componentes del sistema de Material Design realizan un conjunto de tareas predefinidas y tienen determinadas características, como botones. Sin embargo, un botón representa más que una forma de realizar acciones solicitadas por el usuario; también es una expresión visual de forma, tamaño y color que le permite al usuario entender su interactividad y que sucederá algo cuando lo toque o haga clic en él.
En los lineamientos de Material Design se describen los componentes desde el punto de vista de un diseñador. Además, se detallan una gran variedad de funciones básicas que están disponibles en diferentes plataformas, como los elementos anatómicos que conforman cada componente. Por ejemplo, un fondo contiene una capa posterior y su contenido, la capa frontal y su contenido, reglas de movimiento y opciones de visualización. Todos estos componentes se pueden personalizar según las necesidades de cada app, caso de uso y contenido.
Qué compilarás
En este codelab, cambiarás la IU de la app de Shrine a una presentación de dos niveles, que se denomina "fondo". El fondo incluye un menú en el que se detallan categorías que se pueden seleccionar para filtrar los productos que se muestran en la cuadrícula asimétrica. En este codelab, utilizarás los siguientes componentes de Flutter:
- Forma
- Movimiento
- Widgets de Flutter (que ya usaste en los codelabs anteriores)
Android | iOS |
Componentes y subsistemas de MDC-Flutter de este codelab
- Forma
¿Cómo calificarías tu nivel de experiencia con el desarrollo de Flutter?
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
¿Vienes de MDC-103?
Si completaste MDC-103, tu código debería estar listo para este codelab. Ve al paso Agrega el menú de fondo.
¿Empiezas de cero?
La app de inicio se encuentra en el directorio material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series
.
… o clónalo desde GitHub
Para clonar este codelab desde GitHub, ejecuta los siguientes comandos:
git clone https://github.com/material-components/material-components-flutter-codelabs.git cd material-components-flutter-codelabs/mdc_100_series git checkout 104-starter_and_103-complete
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. Deberías ver la página de acceso de Shrine de los codelabs anteriores en tu dispositivo.
Android | iOS |
4. Agrega el menú de fondo
Aparecerá un fondo detrás del resto de los componentes y del contenido. Está compuesto de dos capas: una posterior (que muestra acciones y filtros) y una frontal (que muestra contenido). Puedes usar un fondo para mostrar información y acciones interactivas, como filtros de contenido o navegación.
Quita la barra principal de la aplicación
El widget HomePage será el contenido de nuestra capa frontal. En este momento, tiene una barra de la aplicación. Moverás la barra de la aplicación a la capa posterior y HomePage solo incluirá la AsymmetricView.
En home.dart
, cambia la función build()
para que solo muestre una AsymmetricView.
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
Agrega el widget Backdrop
Crea un widget llamado Backdrop que incluya frontLayer
y backLayer
.
La backLayer
contiene un menú que te permite seleccionar una categoría para filtrar la lista (currentCategory
). Dado que deseamos que la selección del menú persista, harás que Backdrop sea un widget con estado.
Agrega a /lib
un nuevo archivo llamado backdrop.dart
, de la siguiente manera:
import 'package:flutter/material.dart';
import 'model/product.dart';
// TODO: Add velocity constant (104)
class Backdrop extends StatefulWidget {
final Category currentCategory;
final Widget frontLayer;
final Widget backLayer;
final Widget frontTitle;
final Widget backTitle;
const Backdrop({
required this.currentCategory,
required this.frontLayer,
required this.backLayer,
required this.frontTitle,
required this.backTitle,
Key? key,
}) : super(key: key);
@override
_BackdropState createState() => _BackdropState();
}
// TODO: Add _FrontLayer class (104)
// TODO: Add _BackdropTitle class (104)
// TODO: Add _BackdropState class (104)
Ten en cuenta que marcamos algunas propiedades required
. Esta es una práctica recomendada para las propiedades en el constructor que no tienen un valor predeterminado y no pueden ser null
; por lo tanto, no debes olvidarlas.
En la definición de clase de Backdrop, agrega la clase _BackdropState, como se indica a continuación:
// TODO: Add _BackdropState class (104)
class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
// TODO: Add AnimationController widget (104)
// TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
Widget _buildStack() {
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
widget.backLayer,
widget.frontLayer,
],
);
}
@override
Widget build(BuildContext context) {
var appBar = AppBar(
elevation: 0.0,
titleSpacing: 0.0,
// TODO: Replace leading menu icon with IconButton (104)
// TODO: Remove leading property (104)
// TODO: Create title with _BackdropTitle parameter (104)
leading: Icon(Icons.menu),
title: Text('SHRINE'),
actions: <Widget>[
// TODO: Add shortcut to login screen from trailing icons (104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'search',
),
onPressed: () {
// TODO: Add open login (104)
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'filter',
),
onPressed: () {
// TODO: Add open login (104)
},
),
],
);
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: _buildStack(),
);
}
}
La función build()
muestra un elemento Scaffold con una barra de la aplicación de la misma manera que lo hacía HomePage. Sin embargo, el cuerpo de Scaffold es Stack (una pila). Los elementos secundarios de una pila se pueden superponer. El tamaño y la ubicación de cada elemento secundario se especifican en relación con el elemento superior de la pila.
Ahora, agrega una instancia de Backdrop a ShrineApp.
En app.dart
, importa backdrop.dart
y model/product.dart
, como se muestra a continuación:
import 'backdrop.dart'; // New code
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart'; // New code
import 'supplemental/cut_corners_border.dart';
En app.dart,
, muestra un Backdrop
que tenga HomePage
como su frontLayer
para modificar la ruta /
, de la siguiente manera:
// TODO: Change to a Backdrop with a HomePage frontLayer (104)
'/': (BuildContext context) => Backdrop(
// TODO: Make currentCategory field take _currentCategory (104)
currentCategory: Category.all,
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(),
// TODO: Change backLayer field value to CategoryMenuPage (104)
backLayer: Container(color: kShrinePink100),
frontTitle: Text('SHRINE'),
backTitle: Text('MENU'),
),
Guarda el proyecto, se debería ver la página principal y la barra de la app:
Android | iOS |
La backLayer muestra el área rosa en una capa nueva detrás de la página principal de la frontLayer.
Para verificar que la pila tenga un contenedor detrás de una HomePage, puedes usar Flutter Inspector. Debería tener un aspecto similar al siguiente:
Ahora puedes ajustar el diseño y el contenido de las dos capas.
5. Agrega una forma
En este paso, crearás un estilo para la capa frontal con un corte en la esquina superior izquierda.
En Material Design, este tipo de personalización se conoce como una forma. Las superficies de Material pueden tener formas arbitrarias. Estas formas agregan énfasis y estilo a las superficies y se pueden usar para expresar la marca. Las formas rectangulares comunes se pueden personalizar para que tengan esquinas y bordes curvos o angulados, y la cantidad de lados que quieras. Además, pueden ser simétricas o irregulares.
Agrega una forma a la capa frontal
El logotipo angulado de Shrine fue la inspiración para la historia de formas de la app. La historia de formas es el uso común de formas que se aplican en toda una app. Por ejemplo, la forma del logotipo se repite en los elementos con forma de la página de acceso. En este paso, crearás un estilo para la capa frontal con un corte angular en la esquina superior izquierda.
En backdrop.dart
, agrega una clase _FrontLayer nueva de la siguiente manera:
// TODO: Add _FrontLayer class (104)
class _FrontLayer extends StatelessWidget {
// TODO: Add on-tap callback (104)
const _FrontLayer({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return Material(
elevation: 16.0,
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO: Add a GestureDetector (104)
Expanded(
child: child,
),
],
),
);
}
}
Luego, en la función _buildStack()
de _BackdropState, une la capa frontal con una _FrontLayer.
Widget _buildStack() {
// TODO: Create a RelativeRectTween Animation (104)
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
widget.backLayer,
// TODO: Add a PositionedTransition (104)
// TODO: Wrap front layer in _FrontLayer (104)
_FrontLayer(child: widget.frontLayer),
],
);
}
Vuelve a cargar la página.
Android | iOS |
La superficie principal de Shrine tiene una forma personalizada. Debido a la elevación de la superficie, los usuarios pueden ver que hay algo detrás de la capa blanca frontal. A continuación, agregarás movimiento para que los usuarios puedan ver la capa posterior del fondo.
6. Agrega movimiento
El movimiento permite darle vida a tu app. Puede ser amplio y dramático, sutil y mínimo, o bien un efecto intermedio. Sin embargo, debes tener en cuenta que el tipo de movimiento que uses debe ser adecuado para la situación. El movimiento que se implementa en las acciones regulares y repetidas debería ser mínimo y sutil para que las acciones no distraigan al usuario o tarden demasiado cada vez que se ejecutan. No obstante, algunas situaciones son propicias para usar animaciones más llamativas, como la primera vez que el usuario abre una app. Además, algunas animaciones pueden enseñarle al usuario cómo usar la app.
Agrega movimiento de revelación al botón de menú
En la parte superior de backdrop.dart
, fuera del alcance de cualquier clase o función, agrega una constante que represente la velocidad que quieres darle a la animación.
// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;
Agrega un widget AnimationController a _BackdropState, crea una instancia de él en la función initState()
y bórralo en la función dispose()
del estado.
// TODO: Add AnimationController widget (104)
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}
// TODO: Add override for didUpdateWidget (104)
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// TODO: Add functions to get and change front layer visibility (104)
El AnimationController coordina animaciones y te proporciona una API para reproducir, revertir y detener la animación. Ahora necesitas funciones que le den movimiento.
Agrega funciones que determinen y cambien la visibilidad de la capa frontal.
// TODO: Add functions to get and change front layer visibility (104)
bool get _frontLayerVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
void _toggleBackdropLayerVisibility() {
_controller.fling(
velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity);
}
Une la backLayer con un widget ExcludeSemantics, que excluirá los elementos del menú de la backLayer del árbol de semántica cuando la capa posterior no sea visible.
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
...
Cambia la función _buildStack() para que tome un BuildContext y BoxConstraints. Además, incluye una PositionedTransition que tome animación RelativeRectTween de la siguiente manera:
// TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48.0;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;
// TODO: Create a RelativeRectTween Animation (104)
Animation<RelativeRect> layerAnimation = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0, layerTop, 0.0, layerTop - layerSize.height),
end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller.view);
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
// TODO: Add a PositionedTransition (104)
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
child: widget.frontLayer,
),
),
],
);
}
Por último, en lugar de llamar a la función _buildStack para el cuerpo de Scaffold, muestra un widget LayoutBuilder que use _buildStack como compilador.
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: LayoutBuilder(builder: _buildStack),
);
Se retrasó la compilación de la pila de la capa frontal/posterior hasta el tiempo de diseño con LayoutBuilder, de modo que puedas incorporar la altura general real del fondo. LayoutBuilder es un widget especial cuya devolución de llamada del compilador genera restricciones de tamaño.
En la función build()
, convierte el ícono de menú inicial de la barra de la aplicación en un IconButton y úsalo para activar o desactivar la visibilidad de la capa frontal cuando se presiona el botón.
// TODO: Replace leading menu icon with IconButton (104)
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
Vuelve a cargar la página y presiona el botón del menú en el simulador.
Android | iOS |
La capa frontal tiene una animación (se desliza) hacia abajo. Sin embargo, si observas la parte inferior, verás un error rojo y un error de desbordamiento. Esto se debe a que se aprieta la AsymmetricView y se vuelve más pequeña con esta animación, que, a su vez, deja menos espacio para los elementos Column. En algún momento, las columnas no se pueden implementar con el poco espacio que tienen, y se genera un error. Si reemplazas Columns por ListViews, el tamaño de las columnas debería permanecer como la animación.
Une columnas de productos en una ListView
En supplemental/product_columns.dart
, reemplaza Column en OneProductCardColumn
por una ListView.
class OneProductCardColumn extends StatelessWidget {
const OneProductCardColumn({required this.product, Key? key}) : super(key: key);
final Product product;
@override
Widget build(BuildContext context) {
// TODO: Replace Column with a ListView (104)
return ListView(
physics: const ClampingScrollPhysics(),
reverse: true,
children: <Widget>[
const SizedBox(
height: 40.0,
),
ProductCard(
product: product,
),
],
);
}
}
El elemento Column incluye MainAxisAlignment.end
. Para iniciar el diseño desde la parte inferior, marca la opción reverse: true
. Se revertirá el orden de los elementos secundarios para compensar el cambio.
Vuelve a cargar la página y presiona el botón de menú.
Android | iOS |
Ya no aparece la advertencia gris de desbordamiento en OneProductCardColumn. Ahora, debes corregir la otra.
En supplemental/product_columns.dart
, cambia la manera en que se calcula imageAspectRatio
y reemplaza la columna en TwoProductCardColumn
por una ListView.
// TODO: Change imageAspectRatio calculation (104)
double imageAspectRatio = heightOfImages >= 0.0
? constraints.biggest.width / heightOfImages
: 49.0 / 33.0;
// TODO: Replace Column with a ListView (104)
return ListView(
physics: const ClampingScrollPhysics(),
children: <Widget>[
Padding(
padding: const EdgeInsetsDirectional.only(start: 28.0),
child: top != null
? ProductCard(
imageAspectRatio: imageAspectRatio,
product: top!,
)
: SizedBox(
height: heightOfCards,
),
),
const SizedBox(height: spacerHeight),
Padding(
padding: const EdgeInsetsDirectional.only(end: 28.0),
child: ProductCard(
imageAspectRatio: imageAspectRatio,
product: bottom,
),
),
],
);
También se agregó un poco de seguridad a imageAspectRatio
.
Vuelve a cargar la página. Luego, presiona el botón de menú.
Android | iOS |
Ya no hay desbordamientos.
7. Agrega un menú en la capa posterior
Un menú es una lista de elementos de texto que se pueden presionar y que notifica a los objetos de escucha cuando se tocan los elementos de texto. En este paso, agregarás un menú de filtrado de categorías.
Agrega el menú
Agrega el menú a la capa frontal y los botones interactivos a la posterior.
Crea un archivo nuevo llamado lib/category_menu_page.dart
de la siguiente manera:
import 'package:flutter/material.dart';
import 'colors.dart';
import 'model/product.dart';
class CategoryMenuPage extends StatelessWidget {
final Category currentCategory;
final ValueChanged<Category> onCategoryTap;
final List<Category> _categories = Category.values;
const CategoryMenuPage({
Key? key,
required this.currentCategory,
required this.onCategoryTap,
}) : super(key: key);
Widget _buildCategory(Category category, BuildContext context) {
final categoryString =
category.toString().replaceAll('Category.', '').toUpperCase();
final ThemeData theme = Theme.of(context);
return GestureDetector(
onTap: () => onCategoryTap(category),
child: category == currentCategory
? Column(
children: <Widget>[
const SizedBox(height: 16.0),
Text(
categoryString,
style: theme.textTheme.bodyText1,
textAlign: TextAlign.center,
),
const SizedBox(height: 14.0),
Container(
width: 70.0,
height: 2.0,
color: kShrinePink400,
),
],
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Text(
categoryString,
style: theme.textTheme.bodyText1!.copyWith(
color: kShrineBrown900.withAlpha(153)
),
textAlign: TextAlign.center,
),
),
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: const EdgeInsets.only(top: 40.0),
color: kShrinePink100,
child: ListView(
children: _categories
.map((Category c) => _buildCategory(c, context))
.toList()),
),
);
}
}
Es un GestureDetector que ajusta una Column cuyos elementos secundarios son los nombres de las categorías. Para indicar la categoría seleccionada, se usa un subrayado.
En app.dart
, convierte el widget de ShrineApp para que pase del modo con estado a uno sin estado.
- Destaca
ShrineApp.
. - En función del IDE, muestra las siguientes acciones de código:
- Android Studio: Presiona ⌥Intro (en macOS) o alt + Intro
- VS Code: Presiona ⌘. (en macOS) o Ctrl+.
- Selecciona "Convert to StatefulWidget".
- Convierte la clase ShrineAppState en privada (_ShrineAppState). Haz clic con el botón derecho en ShrineAppState y
- Android Studio: selecciona Refactor > Rename.
- VS Code: selecciona el símbolo para cambiar el nombre.
- Ingresa _ShrineAppState para convertir la clase en privada.
En app.dart
, agrega una variable a _ShrineAppState para la categoría seleccionada y una devolución de llamada cuando se presiona.
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
Luego, cambia la capa posterior a una CategoryMenuPage.
En app.dart
, importa la CategoryMenuPage de la siguiente manera:
import 'backdrop.dart';
import 'category_menu_page.dart';
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';
En la función build()
, cambia el campo de backLayer a CategoryMenuPage y el campo de currentCategory para tomar la variable de la instancia.
'/': (BuildContext context) => Backdrop(
// TODO: Make currentCategory field take _currentCategory (104)
currentCategory: _currentCategory,
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(),
// TODO: Change backLayer field value to CategoryMenuPage (104)
backLayer: CategoryMenuPage(
currentCategory: _currentCategory,
onCategoryTap: _onCategoryTap,
),
frontTitle: const Text('SHRINE'),
backTitle: const Text('MENU'),
),
Vuelve a cargar la página y presiona el botón de menú.
Android | iOS |
Si presionas una opción del menú, aún no sucederá nada. Sin embargo, podemos solucionarlo.
En home.dart
, agrega una variable para Category y pásala a la AsymmetricView.
import 'package:flutter/material.dart';
import 'model/product.dart';
import 'model/products_repository.dart';
import 'supplemental/asymmetric_view.dart';
class HomePage extends StatelessWidget {
// TODO: Add a variable for Category (104)
final Category category;
const HomePage({this.category = Category.all, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// TODO: Pass Category variable to AsymmetricView (104)
return AsymmetricView(
products: ProductsRepository.loadProducts(category),
);
}
}
En app.dart
, pasa la _currentCategory
para frontLayer
.
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),
Vuelve a cargar la página. Presiona el botón de menú en el simulador y selecciona una categoría.
Android | iOS |
Presiona el ícono de menú para ver los productos. ¡Se filtraron!
Cierra la capa frontal después de una selección del menú
En backdrop.dart
, agrega una anulación para la función didUpdateWidget()
(ejecutada cada vez que cambia la configuración del widget) en _BackdropState:
// TODO: Add override for didUpdateWidget() (104)
@override
void didUpdateWidget(Backdrop old) {
super.didUpdateWidget(old);
if (widget.currentCategory != old.currentCategory) {
_toggleBackdropLayerVisibility();
} else if (!_frontLayerVisible) {
_controller.fling(velocity: _kFlingVelocity);
}
}
Guarda tu proyecto para activar una recarga en caliente. Presiona el ícono de menú y selecciona una categoría. Se debería cerrar automáticamente el menú, y deberías ver la categoría de elementos elegida. Ahora, también agregarás esa funcionalidad a la capa frontal.
Activa o desactiva la capa frontal
En backdrop.dart
, agrega una devolución de llamada a la capa de fondo para la acción de presionar.
class _FrontLayer extends StatelessWidget {
// TODO: Add on-tap callback (104)
const _FrontLayer({
Key? key,
this.onTap, // New code
required this.child,
}) : super(key: key);
final VoidCallback? onTap; // New code
final Widget child;
Luego, agrega un GestureDetector al elemento secundario de _FrontLayer: los elementos secundarios de Column.
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO: Add a GestureDetector (104)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: 40.0,
alignment: AlignmentDirectional.centerStart,
),
),
Expanded(
child: child,
),
],
),
Luego, implementa la nueva propiedad onTap
en el _BackdropState, en la función _buildStack()
.
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
Vuelve a cargar y presiona la parte superior de la capa frontal. Se debería abrir y cerrar la capa cada vez que presionas la parte superior de la capa frontal.
8. Agrega un ícono de marca
La iconografía de la marca también se extiende a los íconos conocidos. Personalizarás el ícono de revelación y lo combinarás con el título para crear una apariencia de marca única.
Cambia el ícono del botón de menú
Android | iOS |
En backdrop.dart
, crea una clase _BackdropTitle nueva.
// TODO: Add _BackdropTitle class (104)
class _BackdropTitle extends AnimatedWidget {
final void Function() onPress;
final Widget frontTitle;
final Widget backTitle;
const _BackdropTitle({
Key? key,
required Animation<double> listenable,
required this.onPress,
required this.frontTitle,
required this.backTitle,
}) : _listenable = listenable,
super(key: key, listenable: listenable);
final Animation<double> _listenable;
@override
Widget build(BuildContext context) {
final Animation<double> animation = _listenable;
return DefaultTextStyle(
style: Theme.of(context).textTheme.headline6!,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Row(children: <Widget>[
// branded icon
SizedBox(
width: 72.0,
child: IconButton(
padding: const EdgeInsets.only(right: 8.0),
onPressed: this.onPress,
icon: Stack(children: <Widget>[
Opacity(
opacity: animation.value,
child: const ImageIcon(AssetImage('assets/slanted_menu.png')),
),
FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: const Offset(1.0, 0.0),
).evaluate(animation),
child: const ImageIcon(AssetImage('assets/diamond.png')),
)]),
),
),
// Here, we do a custom cross fade between backTitle and frontTitle.
// This makes a smooth animation between the two texts.
Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: const Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.5, 0.0),
).evaluate(animation),
child: backTitle,
),
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: const Offset(-0.25, 0.0),
end: Offset.zero,
).evaluate(animation),
child: frontTitle,
),
),
],
)
]),
);
}
}
_BackdropTitle
es un widget personalizado que reemplazará el widget Text
simple para el parámetro title
del widget AppBar
. Tiene un ícono de menú animado y transiciones animadas entre el título frontal y el posterior. El ícono animado del menú usará un elemento nuevo. Se debe agregar la referencia al nuevo archivo slanted_menu.png
a pubspec.yaml
.
assets:
- assets/diamond.png
# TODO: Add slanted menu asset (104)
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
Quita la propiedad leading
del compilador AppBar
. Esta acción es necesaria para que se renderice el ícono personalizado de la marca en el lugar original del widget leading
. El listenable
de la animación y el controlador de onPress
para el ícono de la marca se pasan al _BackdropTitle
. También se pasan los elementos frontTitle
y backTitle
para que se puedan renderizar en el título del fondo. El parámetro title
de la AppBar
se debería ver de esta forma:
// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
Se crea el ícono de marca en el _BackdropTitle.
Contiene una Stack
de íconos animados: un menú inclinado y un diamante, que se une a un IconButton
para que se pueda presionar. Luego, se une el IconButton
a un SizedBox
a fin de dejar espacio para el movimiento horizontal del ícono.
La arquitectura de Flutter, en la que "todo es un widget", permite que se pueda modificar el diseño de la AppBar
predeterminada sin necesidad de crear un nuevo widget AppBar
personalizado. El parámetro title
, que es originalmente un widget Text
, se puede reemplazar por un _BackdropTitle
más complejo. Dado que el _BackdropTitle
también incluye el Ícono personalizado, toma el lugar de la propiedad leading
, que ahora se puede omitir. Esta sustitución simple de widgets se lleva a cabo sin tener que cambiar ninguno de los otros parámetros, como los íconos de acciones (que siguen funcionando por su cuenta).
Agrega un acceso directo para volver a la pantalla de acceso
En backdrop.dart,
agrega un acceso directo para volver a la pantalla de acceso en los dos íconos finales de la barra de la aplicación. Cambia las etiquetas semánticas de los íconos para que reflejen el propósito nuevo.
// TODO: Add shortcut to login screen from trailing icons (104)
IconButton(
icon: const Icon(
Icons.search,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => LoginPage()),
);
},
),
IconButton(
icon: const Icon(
Icons.tune,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => LoginPage()),
);
},
),
Obtendrás un error si tratas de volver a cargar la pantalla. Importa login.dart
para corregir el error.
import 'login.dart';
Vuelve a cargar la app y presiona los botones de búsqueda o de ajuste a fin de volver a la pantalla de acceso.
9. ¡Felicitaciones!
En el transcurso de estos cuatro codelabs, viste cómo usar los componentes de Material para compilar experiencias del usuario elegantes y únicas que expresen la personalidad y el estilo de la marca.
Próximos pasos
Este codelab, MDC-104, completa la secuencia de codelabs actual. Puedes visitar el catálogo de widgets de Componentes de Material para explorar aún más componentes.
Para ampliar el objetivo, trata de reemplazar el ícono de marca con un AnimatedIcon que tenga una animación entre dos íconos cuando el fondo está visible.
En función de tus intereses, hay muchos otros codelabs de Flutter para que los pruebes. Tenemos otro codelab específico de Material que podría interesarte. Se trata de Cómo compilar transiciones atractivas con el sistema de movimiento de Material para Flutter.