- 原文地址:MDC-104 Flutter: Material Advanced Components (Flutter)
- 原文作者:codelabs.developers.google.com
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:DevMcryYu
- 校對者:iceytea
1. 介紹
Material 元件(MDC)幫助開發者實現 Material Design。MDC 由谷歌團隊的工程師和 UX 設計師創造,為 Android、iOS、Web 和 Flutter 提供很多美觀實用的 UI 元件。
在 MDC-103 教程中,自定義定製了 Material 元件(MDC)的顏色、高度、排版和形狀來給你的應用設定樣式。
Material Design 系統中的元件執行一些預定義的工作並具有一定特徵,例如一個 button。然而一個 button 不僅僅是用來給使用者執行操作的,它可以用其形狀、尺寸和顏色表達一種視覺體驗,讓使用者知道它是可互動的,觸控或點選它時可能會有事情發生。
Material Design 指南以設計師的角度來描述元件。它們描述了跨平臺可用的基本功能以及構成每個元件的基本元素。例如,一個背景包含一個背層內容、前層內容及其本身的內容、運動規則和顯示選項。根據每個應用的需求、用例和內容可以自定義每個元件,包括傳統的檢視、控制元件以及你所處平臺 SDK 的功能。
Material Design 指南命名了很多元件,但不是所有的元件都可以很好的被重用,因此無法在 MDC 中找到它們。你可以自己塑造這樣的經歷,實現使用傳統程式碼自定義你的應用樣式。
你將構建一個
本教程裡,將把 Shrine 應用的 UI 修改成名為“背景”的兩級展示。它包含一個選單,列出了用於過濾在不對稱網格中展示的產品的可選類別。在本教程中,你將使用如下 Flutter 元件:
- 形狀(Shape)
- 動作(Motion)
- Flutter 小部件(在往期教程中所使用的)
這是四篇教程中的最後一篇,它將指導你構建一個名為 Shrine 的應用。我們建議你閱讀每篇教程,跟隨進度逐步完成此專案。
有關教程可以在這裡找到:
此教程中的 MDC-Flutter 元件
- 形狀(Shape)
你將需要
- Flutter SDK
- 安裝好 Flutter 外掛的 Android Studio,或者你喜歡的程式碼編輯器
- 示例程式碼
要在 iOS 上構建和執行 Flutter 應用程式,你需要滿足以下要求:
- 執行 macOS 的計算機
- Xcode 9 或更新版本
- iOS 模擬器,或者 iOS 物理裝置
要在 Android 上構建和執行 Flutter 應用程式,你需要滿足以下要求:
- 執行 macOS、Windows 或 Linux 的計算機
- Android Studio
- Android 模擬器(隨 Android Studio 一起提供)或 Android 物理裝置
2. 安裝 Flutter 環境
前提條件
要開始使用 Flutter 開發移動應用程式,你需要:
- Flutter SDK
- 裝有 Flutter 外掛的 IntelliJ IDE,或者你喜歡的程式碼編輯器
Flutter 的 IDE 工具適用於 Android Studio、IntelliJ IDEA Community(免費)和 IntelliJ IDEA Ultimate。
要在 iOS 上構建和執行 Flutter 應用程式,你需要滿足以下要求:
- 執行 macOS 的計算機
- Xcode 9 或更新版本
- iOS 模擬器,或者 iOS 物理裝置
要在 Android 上構建和執行 Flutter 應用程式,你需要滿足以下要求:
- 執行 macOS,Windows 或者 Linux 的計算機
- Android Studio
- Android 模擬器(隨 Android Studio 一起提供)或 Android 物理裝置
重要提示:如果連線到計算機的 Android 手機上出現“允許 USB 除錯”對話方塊,請啟用始終允許從此計算機選項,然後單擊確定。
在繼續本教程之前,請確保你的 SDK 處於正確的狀態。如果之前安裝過 Flutter SDK,則使用 flutter upgrade
來確保 SDK 處於最新版本。
flutter upgrade
複製程式碼
執行 flutter upgrade
將自動執行 flutter doctor
。如果這是首次安裝 Flutter 且不需升級,那麼請手動執行 flutter doctor
。檢視顯示的所有 ✓ 標記;這將會下載你需要的任何缺少的 SDK 檔案,並確保你的計算機配置無誤以進行 Flutter 的開發。
flutter doctor
複製程式碼
3. 下載教程初始應用程式
從 MDC-103 繼續?
如果你完成了 MDC-103,那麼本教程所需的程式碼應該已經準備就緒。跳轉到:新增背景選單。
從頭開始?
初始程式位於 material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series
目錄下。
...或者從 GitHub 克隆它
從 GitHub 克隆此專案,執行以下命令:
git clone https://github.com/material-components/material-components-flutter-codelabs.git
cd material-components-flutter-codelabs
git checkout 104-starter\_and\_103-complete
複製程式碼
更多幫助:從 GitHub 克隆一個倉庫
正確的分支
教程 MDC-101 到 MDC-104 在前一個基礎上持續構建。MDC-103 的完整程式碼將是 MDC-104 的初始程式碼。程式碼被分成多個分支。要列出 GitHub 中的分支,使用如下命令:
git branch --list
想要檢視完整程式碼,切換到
104-complete
分支。
建立你的專案
以下步驟預設你使用的是 Android Studio (IntelliJ)。
建立專案
-
在終端中,導航到
material-components-flutter-codelabs
-
執行
flutter create mdc_100_series
開啟專案
-
開啟 Android Studio。
-
如果你看到歡迎頁面,單擊開啟已有的 Android Studio 專案。
- 導航到
material-components-flutter-codelabs/mdc_100_series
目錄並單擊開啟,這將開啟此專案。
在構建專案一次之前,你可以忽略在分析中見到的任何錯誤。
- 在左側的專案皮膚中,如果看到測試檔案
../test/widget_test.dart
,刪除它。
- 如果出現上圖提示,安裝所有平臺和外掛更新或 FlutterRunConfigurationType,然後重新啟動 Android Studio。
提示:確保你已安裝 Flutter 和 Dart 外掛。
執行初始程式
以下步驟預設你在 Android 模擬器或真實裝置上進行測試。如果你安裝了 Xcode,則也可以在 iOS 模擬器或裝置上測試。
- 選擇裝置或模擬器
如果 Andorid 模擬器尚未執行,選擇 Tools -> Android -> AVD Manager 來建立並執行一個模擬裝置。如果 AVD 已存在,你可以直接在 IntelliJ 的裝置選擇器中啟動模擬器,如下一步所示。
(對於 iOS 模擬器,如果它尚未執行,通過選擇 Flutter Device Selection -> Open iOS Simulator 來在你的開發裝置上啟動它。)
- 啟動 Flutter 應用:
- 在你的編輯器視窗頂部尋找 Flutter Device Selection 下拉選單,然後選擇裝置(例如,iPhone SE / Android SDK built for <version>)。
- 點選執行圖示()。
如果你無法成功執行此應用程式,停下來解決你的開發環境問題。嘗試導航到
material-components-flutter-codelabs
;如果你在終端中下載 .zip 檔案,導航到material-components-flutter-codelabs-...
然後執行flutter create mdc_100_series
。
成功!上一篇教程中 Shrine 的登陸頁面應該在你的模擬器中執行了。你可以看到 Shrine 的 logo 和它下面的名稱 "Shrine"。
如果應用沒有更新,再次單擊 “Play” 按鈕,或者點選 “Play” 後的 “Stop”。
4. 新增背景選單
背景出現在所有其他內容和元件後面。它由兩層組成:後層(顯示操作和過濾器)和前層(用來顯示內容)。你可以使用背景來顯示互動資訊和操作,例如導航或內容過濾。
移除 home 的應用欄
HomePage 的小部件將成為前層的內容。現在它有一個應用欄。我們將應用欄移動到後層,這樣 HomePage 將只包含 AsymmetricView。
在 home.dart
中,修改 build()
方法使其僅返回一個 AsymmetricView:
// TODO:返回一個 AsymmetricView(104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
複製程式碼
新增背景小部件
建立名為 Backdrop 的小部件,使其包含 frontLayer
和 backLayer
。
backLayer
包含一個選單,它允許你選擇一個類別來過濾列表(currentCategory
)。由於我們希望選單選擇保持不變,因此我們將 Backdrop 繼承 StatefulWidget。
在 /lib
下新增名為 backdrop.dart
的檔案:
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'model/product.dart';
// TODO:新增速度常量(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,
}) : assert(currentCategory != null),
assert(frontLayer != null),
assert(backLayer != null),
assert(frontTitle != null),
assert(backTitle != null);
@override
_BackdropState createState() => _BackdropState();
}
// TODO:新增 _FrontLayer 類(104)
// TODO:新增 _BackdropTitle 類(104)
// TODO:新增 _BackdropState 類(104)
複製程式碼
匯入 meta 包來新增 @required
標記。當建構函式中的屬性沒有預設值且不能為空的時候,用它來提醒你不能遺漏。注意,我們在構造方法後再一次宣告瞭傳入的值的確不是 null
。
在 Backdrop 類定義下新增 _BackdropState
類:
// TODO:新增 _BackdropState 類(104)
class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
// TODO:新增 AnimationController 部件(104)
// TODO:為 _buildStack 新增 BuildContext 和 BoxConstraints 引數(104)
Widget _buildStack() {
return Stack(
key: _backdropKey,
children: <Widget>[
widget.backLayer,
widget.frontLayer,
],
);
}
@override
Widget build(BuildContext context) {
var appBar = AppBar(
brightness: Brightness.light,
elevation: 0.0,
titleSpacing: 0.0,
// TODO:用 IconButton 替換 leading 選單圖示(104)
// TODO:移除 leading 屬性(104)
// TODO:使用 _BackdropTitle 引數建立標題(104)
leading: Icon(Icons.menu),
title: Text('SHRINE'),
actions: <Widget>[
// TODO:新增從尾部圖示到登陸頁面的快捷方式(104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'search',
),
onPressed: () {
// TODO:開啟登入(104)
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'filter',
),
onPressed: () {
// TODO:開啟登入(104)
},
),
],
);
return Scaffold(
appBar: appBar,
// TODO:返回一個 LayoutBuilder 部件(104)
body: _buildStack(),
);
}
}
複製程式碼
build()
方法像 HomePage 一樣返回一個帶有 app bar 的 Scaffold。但是 Scaffold 的主體是一個 Stack。Stack 的孩子可以重疊。每個孩子的大小和位置都是相對於 Stack 的父級指定的。
現在在 ShrineApp 中新增一個 Backdrop 例項。
在 app.dart
中引入 backdrop.dart
及 model/product.dart
:
import 'backdrop.dart'; // 新增程式碼
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart'; // 新增程式碼
import 'supplemental/cut_corners_border.dart';
複製程式碼
在 app.dart
中修改 ShrineApp 的 build()
方法。將 home:
改成以 HomePage 為 frontLayer
的 Backdrop。
// TODO:將 home: 改為使用 HomePage frontLayer 的 Backdrop(104)
home: Backdrop(
// TODO:使 currentCategory 持有 _currentCategory (104)
currentCategory: Category.all,
// TODO:為 frontLayer 傳遞 _currentCategory(104)
frontLayer: HomePage(),
// TODO:將 backLayer 的值改為 CategoryMenuPage(104)
backLayer: Container(color: kShrinePink100),
frontTitle: Text('SHRINE'),
backTitle: Text('MENU'),
),
複製程式碼
如果你點選執行按鈕,你將會看到主頁與應用欄已經出現了:
backLayer 在 frontLayer 的主頁後面插入了一個新的粉色背景。
你可以使用 Flutter Inspector 來驗證在 Stack 裡的主頁後面確實有一個容器。就像這樣:
現在你可以調整兩個層的設計和內容。
5. 新增形狀(Shape)
在本小節,你將為 frontLayer 設定樣式以在其左上角新增一個切片。
Material Design 將此類定製稱為形狀。Material 表面可以具有任意形狀。形狀為表面增加了重點和風格,可用於表達品牌特點。普通的矩形形狀可以定製使其具有彎曲或成角度的角和邊緣,以及任意數量的邊。它們可以是對稱的或不規則的。
為 front layer 新增一個形狀(Shape)
斜角 Shrine logo 激發了 Shrine 應用的形狀故事。形狀故事是應用程式中應用的形狀的常見用法。例如,徽標形狀在應用了形狀的登入頁面元素中回顯。在本小節,您將在左上角使用傾斜切片做為前層設定樣式。
在 backdrop.dart
中,新增新的 _FrontLayer
類:
// TODO:新增 _FrontLayer 類(104)
class _FrontLayer extends StatelessWidget {
// TODO:新增 on-tap 回撥(104)
const _FrontLayer({
Key key,
this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return Material(
elevation: 16.0,
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO:新增 GestureDetector(104)
Expanded(
child: child,
),
],
),
);
}
}
複製程式碼
然後在 BackdropState 的 _buildStack()
方法裡將 front layer 包裹在 _FrontLayer
內:
Widget _buildStack() {
// TODO:建立一個 RelativeRectTween 動畫(104)
return Stack(
key: _backdropKey,
children: <Widget>[
widget.backLayer,
// TODO:新增 PositionedTransition(104)
// TODO:在 _FrontLayer 中包裹 front layer(104)
_FrontLayer(child: widget.frontLayer),
],
);
}
複製程式碼
過載。
我們給 Shrine 的主表面定製了一個形狀。由於表面具有高度,使用者可以看到白色前層後面有東西。讓我們新增一個動作,以便使用者可以看到背景的背景層。
6. 新增動作(Motion)
動作是一種可以讓你的應用變得更真實的方式。它可以是大且誇張的、小且微妙的,亦或是介於兩者之間的。但需要注意的是動作的形式一定要適合使用場景。多次重複的有規律的動作要精細小巧,才不會分散使用者的注意力或佔用太多時間。適當的情況,如使用者第一次開啟應用時,長時的動作可能會更引人注目,一些動畫也可以幫助使用者瞭解如何使用您的應用程式。
為選單按鈕新增顯示動作
在 backdrop.dart
的頂部,其他類函式外,新增一個常量來表示我們需要的動畫執行的速度:
// TODO:新增速度常數(104)
const double _kFlingVelocity = 2.0;
複製程式碼
在 _BackdropState
中新增 AnimationController
部件,在 initState()
函式中例項化它,並將其部署在 state 的 dispose()
函式中:
// TODO:新增 AnimationController 部件(104)
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}
// TODO:重寫 didUpdateWidget(104)
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// TODO:新增函式以確定並改變 front layer 可見性(104)
複製程式碼
部件生命週期
僅在部件成為其渲染樹的一部分之前會呼叫一次
initState()
方法。只有在部件從樹中移除時才會呼叫一次dispose()
方法。
AnimationController 用來配合 Animation,並提供播放、反向和停止動畫的 API。現在我們需要使用某個方法來移動它。
新增函式以確定並改變 front layer 的可見性:
// TODO:新增函式以確定並改變 front layer 的可見性(104)
bool get _frontLayerVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
void _toggleBackdropLayerVisibility() {
_controller.fling(
velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity);
}
複製程式碼
將 backLayer 包裹在 ExcludeSemantics 部件中。當 back layer 不可見時,此部件將從語義樹中剔除 backLayer 的選單項。
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO:將 backLayer 包裹在 ExcludeSemantics 部件中(104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
...
複製程式碼
修改 _buildStack()
方法使其持有一個 BuildContext 和 BoxConstraints。同時包含一個使用 RelativeRectTween 動畫的 PositionedTransition:
// TODO:為 _buildStack 新增 BuildContext 和 BoxConstraints 引數(104)
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48.0;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;
// TODO:建立一個 RelativeRectTween 動畫(104)
Animation<RelativeRect> layerAnimation = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0, layerTop, 0.0, layerTop - layerSize.height),
end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller.view);
return Stack(
key: _backdropKey,
children: <Widget>[
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
// TODO:新增一個 PositionedTransition(104)
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO:在 _BackdropState 上實現 onTap 屬性(104)
child: widget.frontLayer,
),
),
],
);
}
複製程式碼
最後,返回一個使用 _buildStack
作為其 builder 的 LayoutBuilder 部件,而不是為 Scaffold 的主體呼叫 _buildStack
函式:
return Scaffold(
appBar: appBar,
// TODO:返回一個 LayoutBuilder 部件(104)
body: LayoutBuilder(builder: _buildStack),
);
複製程式碼
我們使用 LayoutBuilder 將 front/back 堆疊的構建延遲到佈局階段,以便我們可以合併背景的實際整體高度。LayoutBuilder 是一個特殊的部件,其構建器回撥提供了大小約束。
LayoutBuilder
部件樹通過遍歷葉結點來組織布局。約束在樹下傳遞,但是在葉結點根據約束返回其大小之前通常不會計算大小。葉子點無法知道它的父母的大小,因為它尚未計算。
當部件必須知道其父部件的大小以便自行佈局(且父部件大小不依賴於子部件)時,LayoutBuilder 就派上用場了。它使用一個方法來返回部件。
瞭解有關更多資訊,請檢視 LayoutBuilder 類文件。
在 build()
方法中,將應用欄中的前導選單圖示轉換為 IconButton,並在點選按鈕時使用它來切換 front layer 的可見性。
// TODO:用 IconButton 替換 leading 選單圖示(104)
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
複製程式碼
在模擬器中過載並點選選單按鈕。
front layer 在向下移動(滑動)。但如果向下看,則會出現紅色錯誤和溢位錯誤。這是因為 AsymmetricView 被這個動畫擠壓並變小,反過來使得 Column 的空間更小。最終,Column 不能用給定的空間自行排列並導致錯誤。如果我們用 ListView 替換 Column,則移動時列的尺寸仍然保持不變。
在 ListView 中包裹產品列項
在 supplemental/product_columns.dart
中,將 OneProductCardColumn
的 Column 替換成 ListView:
class OneProductCardColumn extends StatelessWidget {
OneProductCardColumn({this.product});
final Product product;
@override
Widget build(BuildContext context) {
// TODO:用 ListView 替換 Column(104)
return ListView(
reverse: true,
children: <Widget>[
SizedBox(
height: 40.0,
),
ProductCard(
product: product,
),
],
);
}
}
複製程式碼
Column 包含 MainAxisAlignment.end
。要使得從底部開始佈局,使用 reverse: true
。其孩子的順序將翻轉以彌補變化。
過載並點選選單按鈕。
OneProductCardColumn 上的灰色溢位警告消失了!現在讓我們修復另一個問題。
在 supplemental/product_columns.dart
中修改 imageAspectRatio
的計算方式,並將 TwoProductCardColumn
中的 Column 替換成 ListView:
// TODO:修改 imageAspectRatio 的計算方式(104)
double imageAspectRatio =
(heightOfImages >= 0.0 && constraints.biggest.width > heightOfImages)
? constraints.biggest.width / heightOfImages
: 33 / 49;
// TODO:用 ListView 替換 Column(104)
return ListView(
children: <Widget>[
Padding(
padding: EdgeInsetsDirectional.only(start: 28.0),
child: top != null
? ProductCard(
imageAspectRatio: imageAspectRatio,
product: top,
)
: SizedBox(
height: heightOfCards,
),
),
SizedBox(height: spacerHeight),
Padding(
padding: EdgeInsetsDirectional.only(end: 28.0),
child: ProductCard(
imageAspectRatio: imageAspectRatio,
product: bottom,
),
),
],
);
});
複製程式碼
我們還為 imageAspectRatio
新增了一些安全性。
過載。然後點選選單按鈕。
現在已經沒有溢位了。
7. 在 back layer 上新增選單
選單是由可點選文字項組成的列表,當發生點選事件時通知監聽器。在此小節,你將新增一個類別過濾選單。
新增選單
在 front layer 新增選單並在 back layer 新增互動按鈕。
建立名為 lib/category_menu_page.dart
的新檔案:
import 'package:flutter/material.dart';
import 'package:meta/meta.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,
}) : assert(currentCategory != null),
assert(onCategoryTap != null);
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>[
SizedBox(height: 16.0),
Text(
categoryString,
style: theme.textTheme.body2,
textAlign: TextAlign.center,
),
SizedBox(height: 14.0),
Container(
width: 70.0,
height: 2.0,
color: kShrinePink400,
),
],
)
: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Text(
categoryString,
style: theme.textTheme.body2.copyWith(
color: kShrineBrown900.withAlpha(153)
),
textAlign: TextAlign.center,
),
),
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: EdgeInsets.only(top: 40.0),
color: kShrinePink100,
child: ListView(
children: _categories
.map((Category c) => _buildCategory(c, context))
.toList()),
),
);
}
}
複製程式碼
它是一個 GestureDetector,它包含一個 Column,其孩子是類別名稱。下劃線用於指示所選的類別。
在 app.dart
中,將 ShrineApp 部件從 stateless 轉換成 stateful。
- 高亮
ShrineApp.
- 按 alt(option)+ enter
- 選擇 "Convert to StatefulWidget"。
- 將 ShrineAppState 類更改為 private(
_ShrineAppState
)。要從 IDE 主選單執行此操作,請選擇 Refactor > Rename。或者在程式碼中,您可以高亮顯示類名 ShrineAppState,然後右鍵單擊並選擇 Refactor > Rename。輸入_ShrineAppState
以使該類成為私有。
在 app.dart
中,為選擇的類別新增一個變數 _ShrineAppState
,並在點選時新增一個回撥:
// TODO:將 ShrineApp 轉換成 stateful 部件(104)
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
複製程式碼
然後將 back layer 修改為 CategoryMenuPage。
在 app.dart
中引入 CategoryMenuPage:
import 'backdrop.dart';
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'category_menu_page.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';
複製程式碼
在 build()
方法,將 backlayer 欄位修改成 CategoryMenuPage 並讓 currentCategory 欄位持有例項變數。
home: Backdrop(
// TODO:讓 currentCategory 欄位持有 _currentCategory(104)
currentCategory: _currentCategory,
// TODO:為 frontLayer 傳遞 _currentCategory(104)
frontLayer: HomePage(),
// TODO:將 backLayer 修改成 CategoryMenuPage(104)
backLayer: CategoryMenuPage(
currentCategory: _currentCategory,
onCategoryTap: _onCategoryTap,
),
frontTitle: Text('SHRINE'),
backTitle: Text('MENU'),
),
複製程式碼
過載並點選選單按鈕。
你點選了選單選項,然而什麼也沒有發生...讓我們修復它。
在 home.dart
中,為 Category 新增一個變數並將其傳遞給 AsymmetricView。
import 'package:flutter/material.dart';
import 'model/products_repository.dart';
import 'model/product.dart';
import 'supplemental/asymmetric_view.dart';
class HomePage extends StatelessWidget {
// TODO:為 Category 新增一個變數(104)
final Category category;
const HomePage({this.category: Category.all});
@override
Widget build(BuildContext context) {
// TODO:為 Category 新增一個變數並將其傳遞給 AsymmetricView(104)
return AsymmetricView(products: ProductsRepository.loadProducts(category));
}
}
複製程式碼
在 app.dart
中為 frontLayer
傳遞 _currentCategory
:
// TODO:為 frontLayer 傳遞 _currentCategory(104)
frontLayer: HomePage(category: _currentCategory),
複製程式碼
過載。點選模擬器中的選單按鈕並選擇一個類別。
點選選單圖示以檢視產品。他們被過濾了!
選擇選單項後關閉 front layer
在 backdrop.dart
中,為 BackdropState
重寫 didUpdateWidget()
方法:
// TODO:為 didUpdateWidget() 新增重寫方法(104)
@override
void didUpdateWidget(Backdrop old) {
super.didUpdateWidget(old);
if (widget.currentCategory != old.currentCategory) {
_toggleBackdropLayerVisibility();
} else if (!_frontLayerVisible) {
_controller.fling(velocity: _kFlingVelocity);
}
}
複製程式碼
熱過載,然後點選選單圖示並選擇一個類別。選單應該自動關閉,然後你將看到所選擇類別的物品。現在同樣地將這個功能新增到 front layer 。
切換 front layer
在 backdrop.dart
中,給 backdrop layer 新增一個 on-tap 回撥:
class _FrontLayer extends StatelessWidget {
// TODO:新增 on-tap 回撥(104)
const _FrontLayer({
Key key,
this.onTap, // 新增程式碼
this.child,
}) : super(key: key);
final VoidCallback onTap; // 新增程式碼
final Widget child;
複製程式碼
然後將一個 GestureDetector 新增到 _FrontLayer
的孩子 Column 的子節點中:
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO:新增一個 GestureDetector(104)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: 40.0,
alignment: AlignmentDirectional.centerStart,
),
),
Expanded(
child: child,
),
],
),
複製程式碼
然後在 _buildStack()
方法的 _BackdropState
中實現新的 onTap
屬性:
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO:在 _BackdropState 中實現 onTap 屬性(104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
複製程式碼
過載並點選 front layer 的頂部。每次你點選 front layer 頂部時都它應該開啟或者關閉。
8. 新增品牌圖示
品牌肖像也應該延伸到熟悉的圖示。讓我們自定義顯示圖示並將其與我們的標題合併,以獲得獨特的品牌外觀。
修改選單按鈕圖示
在 backdrop.dart
中,新建 _BackdropTitle
類。
// TODO:新增 _BackdropTitle 類(104)
class _BackdropTitle extends AnimatedWidget {
final Function onPress;
final Widget frontTitle;
final Widget backTitle;
const _BackdropTitle({
Key key,
Listenable listenable,
this.onPress,
@required this.frontTitle,
@required this.backTitle,
}) : assert(frontTitle != null),
assert(backTitle != null),
super(key: key, listenable: listenable);
@override
Widget build(BuildContext context) {
final Animation<double> animation = this.listenable;
return DefaultTextStyle(
style: Theme.of(context).primaryTextTheme.title,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Row(children: <Widget>[
// 品牌圖示
SizedBox(
width: 72.0,
child: IconButton(
padding: EdgeInsets.only(right: 8.0),
onPressed: this.onPress,
icon: Stack(children: <Widget>[
Opacity(
opacity: animation.value,
child: ImageIcon(AssetImage('assets/slanted_menu.png')),
),
FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: Offset(1.0, 0.0),
).evaluate(animation),
child: ImageIcon(AssetImage('assets/diamond.png')),
)]),
),
),
// 在這裡,我們在 backTitle 和 frontTitle 之間是實現自定義的交叉淡入淡出效果
// 這使得兩個文字之間能夠平滑過渡。
Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: Offset(0.5, 0.0),
).evaluate(animation),
child: backTitle,
),
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset(-0.25, 0.0),
end: Offset.zero,
).evaluate(animation),
child: frontTitle,
),
),
],
)
]),
);
}
}
複製程式碼
_BackdropTitle
是一個自定義部件,它將替換 AppBar
裡 title
引數的 Text
部件。它有一個動畫選單圖示和前後標題之間的動畫過渡。動畫選單圖示將使用新資源。因此必須將對新 slanted_menu.png
的引用新增到 pubspec.yaml
中。
assets:
- assets/diamond.png
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
複製程式碼
移除 AppBar
builder 中的 leading
屬性。這樣才能在原始 leading
部件的位置顯示自定義品牌圖示。listenable
動畫和品牌圖示的 onPress
處理將傳遞給 _BackdropTitle
。frontTitle
和 backTitle
也會被傳遞,以便將它們顯示在背景標題中。AppBar
的 title
引數如下所示:
// TODO:使用 _BackdropTitle 引數建立標題(104)
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
複製程式碼
品牌圖示在 _BackdropTitle
中建立。它包含一組動畫圖示:傾斜的選單和鑽石,它包裹在 IconButton
中,以便可以按下它。然後將 IconButton
包裝在 SizedBox
中,以便為圖示水平運動騰出空間。
Flutter 的 "everything is a widget" 架構允許更改預設 AppBar
的佈局,而無需建立全新的自定義 AppBar
小部件。title
引數最初是一個 Text
部件,可以用更復雜的 _BackdropTitle
替換。由於 _BackdropTitle
還包含自定義圖示,因此它取代了 leading
屬性,現在可以省略。這個簡單的部件替換是在不改變任何其他引數的情況下完成的,例如動作圖示,它們可以繼續執行。
新增返回登入螢幕的快捷方式
在 backdrop.dart
中,從應用欄中的兩個尾部圖示向登入螢幕新增一個快捷方式:更改圖示的 semanticLabel
以反映其新用途。
// TODO:新增從尾部圖示到登陸頁面的快捷方式(104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'login', // 新增程式碼
),
onPressed: () {
// TODO:開啟登陸(104)
Navigator.push(
context,
MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
);
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'login', // 新增程式碼
),
onPressed: () {
// TODO:開啟登入(104)
Navigator.push(
context,
MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
);
},
),
複製程式碼
如果你嘗試過載將收到錯誤訊息。匯入 login.dart
以修復錯誤:
import 'login.dart';
複製程式碼
過載應用並點選搜尋或調整按鈕以返回登入螢幕。
9. 總結
通過四篇教程,你已經瞭解瞭如何使用 Material 元件來構建表達品牌個性和風格的獨特,優雅的使用者體驗。
完整的 MDC-104 應用可在
104-complete
分支中找到。您可以使用該分支中的版本測試你的應用。
下一步
MDC-104 到此已經完成。你可以訪問 Flutter Widget 目錄以在 MDC-Flutter 中探索更多元件。
對於進階的目標,嘗試使用 AnimatedIcon 替換品牌圖示。
要了解如何將應用連線到 Firebase 以獲得後端支援,請參閱 Flutter 中的 Firebase。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。