1. 簡介
Material Design 元件 (MDC) 可協助開發人員實作質感設計。MDC 是由 Google 的工程師和使用者體驗設計師團隊所開發,提供數十種美觀實用的 UI 元件,適用於 Android、iOS、網頁和 Flutter。material.io/develop |
在程式碼研究室 MDC-103 中,您已自訂 Material Design 元件 (MDC) 的顏色、高度、字體排版和形狀,藉此設定應用程式的樣式。
質感設計系統中的元件會執行一組預先定義的工作,並具有特定特性,例如按鈕。然而,按鈕不只是讓使用者執行動作的方法,同時也是形狀、大小及顏色的視覺呈現方式,可讓使用者瞭解這是有互動性質,輕觸或點擊就會發生。
Material Design 指南會從設計師的角度說明元件。說明各平台可用的基本功能,以及組成每個元件的原子元素。舉例來說,背景含有一個後層及其內容、前層及其內容、動態規則和顯示選項。您可根據每個應用程式的需求、用途和內容,自訂上述每個元件。
建構項目
在本程式碼研究室中,您會將 Shrine 應用程式中的 UI 變更為「背景幕」的雙層簡報。背景包含選單,列出可選取的類別,用於篩選不對稱格狀圖片中顯示的產品。在本程式碼研究室中,您將使用下列資料:
- 圖案
- 動作
- Flutter 小工具 (您在先前程式碼研究室中使用過的)
Android | iOS |
本程式碼研究室中的 Material Flutter 元件和子系統
- 圖案
針對 Flutter 開發經驗,您會給予什麼評價?
2. 設定 Flutter 開發環境
您需要使用兩項軟體:Flutter SDK 和編輯器。
您可以使用下列任一裝置執行程式碼研究室:
- 將實體 Android 或 iOS 裝置接上電腦,並設為開發人員模式。
- iOS 模擬器 (需要安裝 Xcode 工具)。
- Android Emulator (需要在 Android Studio 中設定)。
- 瀏覽器 (偵錯時必須使用 Chrome)。
- 以 Windows、Linux 或 macOS 桌面應用程式形式。您必須在要部署的平台上進行開發。因此,如果您想開發 Windows 電腦應用程式,就必須在 Windows 上進行開發,才能存取適當的建構鏈結。如要進一步瞭解作業系統的特定需求,請參閱 docs.flutter.dev/desktop。
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/mdc_100_series git checkout 104-starter_and_103-complete
開啟專案並執行應用程式
- 在您選擇的編輯器中開啟專案。
- 按照開始使用:試用編輯器中的「執行應用程式」操作說明,為所選編輯器執行操作。
大功告成!您應該會在裝置上看到先前程式碼研究室的 Shrine 登入頁面。
Android | iOS |
4. 新增背景幕選單
背景會出現在所有其他內容和元件的後面。這個程序包含兩個圖層:後層 (顯示動作和篩選器) 和前端 (顯示內容)。您可以使用背景顯示互動資訊和動作,例如導覽或內容篩選器。
移除主畫面應用程式列
HomePage 小工具會成為我們前端的內容,這個畫面現在多了應用程式列。我們要將應用程式列移至後層,HomePage 只會含有 AsymmetricView。
在 home.dart
中,將 build()
函式變更為只傳回 AsymmetricView:
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
新增背景小工具
建立名為「Backdrop」的資訊方塊,其中包含 frontLayer
和 backLayer
。
backLayer
包含選單,可讓您選取類別來篩選清單 (currentCategory
)。由於我們希望選取的選單持續存在,因此會將背景設為有狀態的小工具。
在 /lib
中新增名為 backdrop.dart
的檔案:
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)
請注意,我們會標示特定屬性 required
。如果屬性的建構函式沒有預設值,因此不能是 null
,那麼這是最佳做法。
在 Backdrop 類別定義下方,新增 _BackdropState 類別:
// 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(),
);
}
}
build()
函式會傳回 Scaffold,其中包含應用程式列,就像 HomePage 一樣。但是 Scaffold 的主體是堆疊。堆疊的子項可以重疊。每個子項的大小和位置皆會相對於堆疊的父項指定。
現在將 Backdrop 執行個體新增至 ShrineApp。
在 app.dart
中,匯入 backdrop.dart
和 model/product.dart
:
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';
在 app.dart,
中,傳回 HomePage
做為 frontLayer
的 Backdrop
,藉此修改 /
路徑:
// 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'),
),
儲存專案,您應該會看到首頁已顯示,而應用程式列如下:
Android | iOS |
backLayer 會在 frontLayer 首頁後方的新圖層中顯示粉紅色區域。
您可以使用 Flutter Inspector 驗證 Stack 確實在 HomePage 後方有一個容器。內容應類似下方範例:
您現在可以調整圖層的設計和內容。
5. 新增形狀
在這個步驟中,您將在左上角新增前圖層的樣式。
Material Design 將這類自訂項目稱為形狀。Material 表面可具有任意形狀。形狀可為表面增添強調效果和風格,也可用於宣傳品牌。您可以自訂一般矩形形狀,讓邊角和邊緣呈現弧形或角形,也可以自訂邊數。這些圖案可以對稱或不規則。
在前層加入形狀
有角形的神社標誌激勵了神社應用程式的形狀故事。形狀故事是應用程式套用形狀的常見用途。舉例來說,標誌形狀會在已套用形狀的登入頁面元素中相稱。在這個步驟中,您將在左上角設定側面裁剪樣式。
在 backdrop.dart
中新增 _FrontLayer 類別:
// 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,
),
],
),
);
}
}
接著,在 _BackdropState 的 _buildStack()
函式中,將前圖層納入 _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),
],
);
}
重新載入。
Android | iOS |
我們已為 Shrine 的主要介面指定自訂形狀。不過,我們希望這個設計在視覺上與應用程式列建立連結。
變更應用程式列顏色
在 app.dart
中,將 _buildShrineTheme()
函式變更為以下內容:
ThemeData _buildShrineTheme() {
final ThemeData base = ThemeData.light(useMaterial3: true);
return base.copyWith(
colorScheme: base.colorScheme.copyWith(
primary: kShrinePink100,
onPrimary: kShrineBrown900,
secondary: kShrineBrown900,
error: kShrineErrorRed,
),
textTheme: _buildShrineTextTheme(base.textTheme),
textSelectionTheme: const TextSelectionThemeData(
selectionColor: kShrinePink100,
),
appBarTheme: const AppBarTheme(
foregroundColor: kShrineBrown900,
backgroundColor: kShrinePink100,
),
inputDecorationTheme: const InputDecorationTheme(
border: CutCornersBorder(),
focusedBorder: CutCornersBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrineBrown900,
),
),
floatingLabelStyle: TextStyle(
color: kShrineBrown900,
),
),
);
}
熱重新啟動。現在應該會顯示新的彩色應用程式列。
Android | iOS |
基於這項變更,使用者可以看到前端白色圖層後方的項目。請加入動態效果,方便使用者查看背景幕的後層。
6. 新增動態效果
動態效果是讓您的應用程式變得生動活潑的方法。可以是大型、戲劇化、細微或簡約,或介於兩者之間。不過請注意,您採用的動態效果應該適用於情境。套用至重複一般動作的動態效果應簡潔有力,以免動作幹擾使用者或定期花太多時間。不過,有些情況 (例如使用者首次開啟應用程式時) 看起來更吸引人,而一些動畫可以向使用者說明應用程式的使用方法。
在選單按鈕中加入顯示動態效果
在 backdrop.dart
頂端的任何類別或函式範圍外,加入常數來代表所需動畫的速度:
// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;
將 AnimationController 小工具新增至 _BackdropState,在 initState()
函式中將其執行個體化,並在狀態的 dispose()
函式中予以處理:
// 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)
AnimationController 會協調動畫,並提供 API 來播放、倒轉及停止動畫。接下來,我們需要讓它移動的函式。
新增可以決定及變更前層顯示設定的函式:
// 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);
}
將 backLayer 包裝在 ExcludeSemantics 小工具中。當後層無法顯示時,這個小工具會從語意樹狀結構中排除 backLayer 的選單項目。
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
...
變更 _buildStack() 函式以採用 BuildContext 和 BoxConstraint。此外,請加入採用 RelativeRectTween 動畫的 PositionedTransition:
// 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,
),
),
],
);
}
最後,傳回使用 _buildStack 做為建構工具的 LayoutBuilder 小工具,而非針對 Scaffold 內文呼叫 _buildStack 函式:
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: LayoutBuilder(builder: _buildStack),
);
我們已將前端/返回圖層堆疊的建構延後到使用 LayoutBuilder 的版面配置時間,以便加入背景的實際整體高度。LayoutBuilder 是一種特殊小工具,它的建構工具回呼會提供大小限制。
在 build()
函式中,將應用程式列中的前導選單圖示轉換為 IconButton,並在輕觸按鈕時使用該按鈕切換前層的顯示/隱藏狀態。
// TODO: Replace leading menu icon with IconButton (104)
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
重新載入,然後輕觸模擬器中的選單按鈕。
Android | iOS |
前層會以動畫效果 (滑動) 向下移動。但你也可以往下看,就會發生紅色錯誤和溢位錯誤。這是因為 AsymmetricView 會因這項動畫而被擠壓,變得更小,進而讓 Columns 的空間變小。最後,資料欄無法以指定的空間進行版面配置,因此會導致錯誤。如果利用 ListViews 取代 欄,欄的大小應與其動畫一樣。
納入 ListView 中的產品欄
在 supplemental/product_columns.dart
中,將 OneProductCardColumn
中的資料欄替換為 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>[
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 550,
),
child: ProductCard(
product: product,
),
),
const SizedBox(
height: 40.0,
),
],
);
}
}
此欄包含 MainAxisAlignment.end
。如要從底部開始版面配置,請標記 reverse: true
。子項的順序已撤銷,以彌補變更的不足。
重新載入並輕觸選單按鈕。
Android | iOS |
OneProductCardColumn 上的灰色溢出警告已消失!接下來,我們來修正另一個問題。
在 supplemental/product_columns.dart
中,變更 imageAspectRatio
的計算方式,並將 TwoProductCardColumn
中的資料欄替換為 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,
),
),
],
);
我們也為 imageAspectRatio
多添一層安全保障。
重新載入。然後輕觸選單按鈕。
Android | iOS |
沒有溢位現象。
7. 在背景圖層上新增選單
選單是可點選的文字項目清單,可在使用者觸碰文字項目時通知監聽器。在這個步驟中,您將新增類別篩選選單。
新增選單
將選單新增至前層,並將互動按鈕新增至後層。
建立名為 lib/category_menu_page.dart
的新檔案:
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.bodyLarge,
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.bodyLarge!.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()),
),
);
}
}
這是一個 GestureDetector,用來包裝子項為類別名稱的資料欄。底線則表示所選類別。
在 app.dart
中,將 ShrineApp 小工具從無狀態轉換為有狀態。
- 標示
ShrineApp.
- 根據您的 IDE 顯示程式碼動作:
- Android Studio:按下 ⌥Enter (macOS) 或 Alt + Enter
- VS 代碼:按下 ⌘. (macOS) 或 Ctrl+。
- 選取 [Convert to StatefulWidget]。
- 將 ShrineAppState 類別設為私有 (_ShrineAppState)。在 ShrineAppState 上按一下滑鼠右鍵,然後
- Android Studio:依序選取「Refactor」>「Rename」
- VS Code:選取「Rename Symbol」
- 輸入 _ShrineAppState 即可將類別設為私有。
在 app.dart
中,為所選類別新增變數至 _ShrineAppState,並在輕觸時回呼:
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
然後將後層變更為 CategoryMenuPage。
在 app.dart
中,匯入 CategoryMenuPage:
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';
在 build()
函式中,將 backLayer 欄位變更為 CategoryMenuPage,並使用 currentCategory 欄位取得例項變數。
'/': (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'),
),
重新載入並輕觸「選單」按鈕。
Android | iOS |
如果您輕觸選單選項,目前還沒有任何動作。讓我們一起解決這個問題!
在 home.dart
中,新增 Category 的變數並傳送至 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),
);
}
}
在 app.dart
中,傳遞 frontLayer
的 _currentCategory
:
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),
重新載入。輕觸模擬器中的選單按鈕,然後選取類別。
Android | iOS |
已篩除!
關閉選取選單後的前層
在 backdrop.dart
中,為 _BackdropState 中的 didUpdateWidget()
(每當小工具設定變更時呼叫) 函式新增覆寫值:
// 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);
}
}
儲存專案,觸發熱重載。輕觸選單圖示,然後選取類別。選單應會自動關閉,您應該會看到所選項目的類別。現在,您也需要在前端新增該功能。
切換前層
在 backdrop.dart
中,為背景圖層新增輕觸回呼:
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;
接著,將 MenuDetector 新增至 _FrontLayer 的子項: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,
),
],
),
接著,在 _buildStack()
函式的 _BackdropState 中實作新的 onTap
屬性:
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
重新載入並輕觸前景圖層頂端。每次您輕觸前層頂端時,應該都會開啟並關閉圖層。
8. 新增品牌圖示
品牌圖示也適用於常見的圖示。我們來製作揭露圖示,並將其與標題合併,打造獨特的品牌風格。
變更選單按鈕圖示
Android | iOS |
在 backdrop.dart
中,建立新類別 _BackdropTitle。
// 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.titleLarge!,
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
是自訂小工具,會取代 AppBar
小工具的 title
參數中純文字 Text
小工具。其有動畫選單圖示,以及前後標題之間的動畫轉場效果。動畫選單圖示會使用新的素材資源。您必須將新 slanted_menu.png
的參照新增至 pubspec.yaml
。
assets:
- assets/diamond.png
# TODO: Add slanted menu asset (104)
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
移除 AppBar
建構工具中的 leading
屬性。您必須移除原始 leading
小工具,才能在原始小工具的位置顯示自訂品牌圖示。動畫 listenable
和品牌圖示的 onPress
處理常式會傳遞至 _BackdropTitle
。也會傳遞 frontTitle
和 backTitle
,以便在背景幕標題中算繪。AppBar
的 title
參數應如下所示:
// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
品牌圖示是在 _BackdropTitle.
中建立,其中包含一個動畫圖示 Stack
:傾斜選單和鑽石,位於 IconButton
中,可供使用者點選。接著,IconButton
會包裝在 SizedBox
中,為水平圖示動作騰出空間。
Flutter 的「Everything is a widget」架構允許變更預設 AppBar
的版面配置,而不必建立新的自訂 AppBar
小工具。title
參數原本是 Text
小工具,可改用更複雜的 _BackdropTitle
。由於 _BackdropTitle
也包含自訂圖示,因此會取代 leading
屬性,現在可以省略了。您可以透過簡單的設定,在不需要變更任何其他參數的情況下,替換小工具,例如繼續自行運作的動作圖示。
新增返回登入畫面的捷徑
在 backdrop.dart,
中,從應用程式列的兩個結尾圖示新增返回登入畫面的捷徑:根據新用途變更圖示的語意標籤。
// 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()),
);
},
),
如果嘗試重新載入,便會收到錯誤訊息。匯入 login.dart
來修正錯誤:
import 'login.dart';
重新載入應用程式,然後輕觸搜尋或調音按鈕,即可返回登入畫面。
9. 恭喜!
在四個程式碼研究室的課程中,您已瞭解如何使用 Material Design 元件,打造獨特且優雅的使用者體驗,展現品牌個性和風格。
後續步驟
本程式碼研究室 MDC-104 會完成這個程式碼研究室系列。您可以前往 Material 元件小工具目錄,探索 Material Flutter 中更多的元件。
針對延展目標,請嘗試將品牌圖示換成 AnimatedIcon,該圖示會在背景顯示時在兩個圖示之間建立動畫效果。
您可以根據興趣嘗試其他許多 Flutter 程式碼研究室。我們也提供另一個 Material Design 專屬程式碼研究室,您可能有興趣瞭解:使用 Material Motion for Flutter 建構精美的轉場效果。