目錄
1. 路由管理(頁面跳轉(zhuǎn))
2. 資源管理
1. 路由管理(頁面跳轉(zhuǎn))
路由Route
在移動開發(fā)中通常指頁面(iOS的UIViewController、Android的Activity)。
導(dǎo)航器Navigator(路由管理)
管理路由之間如何跳轉(zhuǎn)。
維護一個路由棧,路由入棧操作(push)會打開新頁面,路由出棧操作(pop)會關(guān)閉頁面,路由管理主要是指如何管理路由棧。
/*
嵌套導(dǎo)航器
比如Tabbar,每個Tab內(nèi)都有一個獨立的導(dǎo)航器。
應(yīng)用里的所有導(dǎo)航器為樹狀結(jié)構(gòu),有一個根導(dǎo)航器,同一節(jié)點下的各兄弟并行導(dǎo)航。
*/
1. 跳轉(zhuǎn)到下一頁面(Navigator.push)
// push方法返回值類型為Future。
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new HelloWidget()), // 需要一個Route
);
等價(第一個參數(shù)是context都可以這樣等價替換)
Navigator.of(context).push(
new MaterialPageRoute(builder: (context) => new HelloWidget()),
);
2. 返回(Navigator.pop)
返回到上一頁面
Navigator.pop(context);
返回到根頁面
Navigator.of(context,rootNavigator:true).pop();
示例(在模版計數(shù)器示例中,做如下修改)
1. 創(chuàng)建一個新路由
class NewRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("New route"),
),
body: new Center(
child: new RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: new Text('Go back!'),
),
),
);
}
}
2. 在_MyHomePageState.build方法中添加一個按鈕:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
... // 省略無關(guān)代碼
FlatButton(
child: Text("open new route"),
textColor: Colors.blue,
onPressed: () {
// 導(dǎo)航到新路由
Navigator.push( context,
MaterialPageRoute(builder: (context) {
return NewRoute();
}));
},
),
],
)
- Navigator導(dǎo)航器
通過棧來管理路由,提供了打開和退出路由的方法。
通常當前屏幕顯示的頁面是棧頂路由。
1. Future push(BuildContext context, Route route)
將指定路由入棧(打開新頁面)。
返回值是一個Future對象,用以接收新路由出棧(頁面關(guān)閉)時的返回數(shù)據(jù)。
2. bool pop(BuildContext context, [result])
將棧頂路由出棧(關(guān)閉當前頁)。
result為頁面關(guān)閉時返回給上一頁面的數(shù)據(jù)。
3. 其他方法
Navigator.replace、Navigator.popUntil
- MaterialPageRoute (繼承自PageRoute抽象類)
MaterialPageRoute({
// 構(gòu)建路由頁,返回值是一個widget。
WidgetBuilder builder,
// 路由頁的配置信息(路由頁名稱、是否首頁)
RouteSettings settings,
// 設(shè)置為false后,當入棧一個新路由后會釋放內(nèi)存中原來的路由頁。
bool maintainState = true,
// 是否全屏
// 在iOS中,如果為true新頁面會從屏幕底部滑入(而不是水平方向)。
bool fullscreenDialog = false,
})
/*
Android當打開新頁面時,新的頁面會從屏幕底部滑動到屏幕頂部;當關(guān)閉頁面時,當前頁面會從屏幕頂部滑動到屏幕底部后消失,同時上一個頁面會顯示到屏幕上。
iOS當打開頁面時,新的頁面會從屏幕右側(cè)邊緣一致滑動到屏幕左邊,直到新頁面全部顯示到屏幕上,而上一個頁面則會從當前屏幕滑動到屏幕左側(cè)而消失;當關(guān)閉頁面時,正好相反,當前頁面會從屏幕右側(cè)滑出,同時上一個頁面會從屏幕左側(cè)滑入。
可以針對不同平臺,實現(xiàn)動畫風格一致的路由切換動畫。
*/
PageRoute類
占有整個屏幕空間的一個模態(tài)路由頁面,定義了路由構(gòu)建及切換時過渡動畫的相關(guān)接口及屬性。
如果想自定義路由切換動畫,可以繼承PageRoute來實現(xiàn)。
- 路由傳值(跳轉(zhuǎn)時通常需要傳遞數(shù)據(jù))
例如:打開商品詳情頁時需要一個商品id展示對應(yīng)商品詳情;填寫訂單時需要選擇收貨地址,打開地址選擇頁并選擇地址后,將所選地址返回給訂單頁。
示例(雙向傳遞數(shù)據(jù))
class RouterTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
onPressed: () async {
var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return TipRoute(
// 路由參數(shù)
text: "hello world",
);
},
),
);
// 點擊返回按鈕,命令行輸出 路由返回值: 我是返回值。點擊箭頭,命令行輸出 路由返回值: null。
print("路由返回值: $result");
},
child: Text("打開提示頁"),
),
);
}
}
// 接受一個文本參數(shù)用來展示。點擊“返回”按鈕后返回上一個路由時 傳遞了文本數(shù)據(jù)。
class TipRoute extends StatelessWidget {
TipRoute({this.text});
final String text;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("提示"),
),
body: Padding(
padding: EdgeInsets.all(18),
child: Center(
child: Column(
children: <Widget>[
Text(text),
RaisedButton(
onPressed: () => Navigator.pop(context, "我是返回值"),
child: Text("返回"),
)
],
),
),
),
);
}
}
- 命名路由(有名字的路由)
給路由起名字后,可以通過路由名來打開新路由。
通過路由名打開新路由時,應(yīng)用會根據(jù)路由名在【路由表】中查找并調(diào)用對應(yīng)的WidgetBuilder回調(diào)函數(shù)生成并返回路由。
路由表的定義如下:
// MaterialApp的一個屬性。
// key為路由名,value是個builder回調(diào)函數(shù)(用于生成并返回相應(yīng)的路由)。
Map<String, WidgetBuilder> routes;
好處:
1. 語義化更明確。
2. 維護更方便。
如果使用匿名路由:不僅需要import新路由頁的dart文件,而且代碼非常分散不方便管理。還需要知道頁面的構(gòu)建方式,且變化后需要涉及該頁面的跳轉(zhuǎn)都需要修改,代碼耦合性嚴重。
3. 可以通過onGenerateRoute(路由攔截器)做一些全局的路由跳轉(zhuǎn)前的處理邏輯。
使用:
1. 首先注冊一個路由表,提供對應(yīng)關(guān)系(路由名--->路由)
在MaterialApp中添加routes屬性:
MaterialApp(
title: ProjectConfig.packageInfo.appName,
theme: ProjectTheme.theme,
routes: {
'/':(context)=>BootstrapPage(),
'/login':(context)=>LoginPage(),
'/register':(context)=>RegisterPage(),
'/tab':(context)=>TabPage(),
},
// navigatorKey: // GlobalKey<NavigatorState>對象,全局存儲導(dǎo)航的狀態(tài),
),
2. 通過路由名打開新路由頁
// 第三個參數(shù)可選,用于傳遞值。
// 在導(dǎo)航欄上會顯示返回按鈕
Navigator.pushNamed(context, "/new_page");
3. 在新路由頁可以
使用替換路由打開新頁面:
// 點擊返回按鈕或者pop后會返回到“上上頁”,即調(diào)用Navigator.pushNamed的頁面
Navigator.pushReplacementNamed(context, "/new_page");
返回到上一頁面:
Navigator.pop(context);
返回根路由:
Navigator.of(context,rootNavigator:true).pop();
返回根路由:
Navigator.pushAndRemoveUntil(
context,
new MaterialPageRoute(builder: (context)=>new HomeRoute()),
(route)=>route==null
);
4. 路由生成鉤子(onGenerateRoute)
1. MaterialApp的一個屬性,只會對命名路由生效。
2. 可用來統(tǒng)一處理跳轉(zhuǎn)邏輯(參數(shù)傳遞、無效路由、權(quán)限訪問)。
3. 當調(diào)用Navigator.pushNamed(...)打開命名路由時,如果指定的路由名在路由表中已注冊,則會調(diào)用路由表中的builder函數(shù)來生成路由組件;如果路由表中沒有注冊,才會調(diào)用onGenerateRoute來生成路由。
5. 命名路由參數(shù)傳遞(早期版本并不支持)
1. 注冊路由表
routes:{
"new_page":(context) => NewRoute(),
} ,
2. 傳遞值
Navigator.of(context).pushNamed("new_page", arguments: "hi");
3. 獲取值
方式1:(NewRoute的build方法中)
Widget build(BuildContext context) {
// 獲取路由參數(shù)
var args=ModalRoute.of(context).settings.arguments;
//...省略無關(guān)代碼
}
方式2:
如果NewRoute接受一個text 參數(shù),在不改變NewRoute源碼的前提下,使用路由名來打開它需要:
routes: {
"new_page": (context){
return TipRoute(text: ModalRoute.of(context).settings.arguments);
},
},
/*
看一下pushNamed方法和pop方法的實現(xiàn):
Future<T?> pushNamed<T extends Object?>(
String routeName, {
Object? arguments,
}) {
return push<T>(_routeNamed<T>(routeName, arguments: arguments)!);
}
void pop<T extends Object?>([ T? result ]) {
...
}
*/
示例(創(chuàng)建了一個路由管理類來集中管理Route。使用onGenerateRoute來統(tǒng)一處理跳轉(zhuǎn))
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../app.dart';
import '../login.dart';
import '../not_found.dart';
import '../splash.dart';
class RouterTable {
static String splashPath = 'splash';
static String loginPath = 'login';
static String homePath = '/';
static String notFoundPath = '404';
static Map<String, WidgetBuilder> routeTables = {
// 404頁面
notFoundPath: (context) => NotFound(),
// 啟動頁
splashPath: (context) => Splash(),
// 登錄
loginPath: (context) => LoginPage(),
// 首頁
homePath: (context) => AppHomePage(),
};
// 路由攔截
static Route onGenerateRoute<T extends Object>(RouteSettings settings) {
return CupertinoPageRoute<T>(
settings: settings,
builder: (context) {
String name = settings.name;
if (routeTables[name] == null) {
name = notFoundPath;
}
Widget widget = routeTables[name](context);
return widget;
},
);
}
}
====================
使用
import 'package:flutter/material.dart';
import 'routers/router_table.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final GlobalKey navigationKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigationKey,
onGenerateRoute: RouterTable.onGenerateRoute,
initialRoute: RouterTable.splashPath,
);
}
}
示例(routes注冊路由表、initialRoute首頁)
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 注冊路由表
routes:{
"new_page":(context) => NewRoute(),
... // 省略其它路由注冊信息
} ,
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
修改FlatButton的onPressed,修改跳轉(zhuǎn)邏輯為:
onPressed: () {
Navigator.pushNamed(context, "/new_page");
//Navigator.push(context,
// MaterialPageRoute(builder: (context) {
// return NewRoute();
//}));
},
/*
也可以將首頁注冊為命名路由
MaterialApp(
title: 'Flutter Demo',
// 設(shè)置初始路由:將名為"/"的路由作為應(yīng)用的首頁(默認就是‘/’,不用再次賦值)。
// 設(shè)置initialRoute后不再需要設(shè)置home。
initialRoute:"/",
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 注冊路由表
routes:{
"/new_page":(context) => NewRoute(),
"/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注冊首頁路由
}
);
*/
示例(onGenerateRoute統(tǒng)一設(shè)置權(quán)限)
MaterialApp(
// 在該回調(diào)中進行統(tǒng)一的權(quán)限控制
// Route<dynamic> Function(RouteSettings settings)
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// 如果訪問的路由頁需要登錄,但當前未登錄,則直接返回登錄頁路由,
// 引導(dǎo)用戶登錄;其它情況則正常打開路由。
}
);
}
);
示例(onGenerateRoute統(tǒng)一處理傳參)
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// 通常放在單獨的文件中
final routes={
'/page1':(context)=>HomePageWidget(),
'/page2':(context,{arguments})=>MyPageWidget(arguments:arguments),
};
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.lightBlue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute:"/page1",
// 通常將該方法抽離和routes位于單獨的文件中
onGenerateRoute: (RouteSettings settings){
final String name = settings.name; // 路由名
final Function pageContentBuilder = this.routes[name];
if(settings.arguments!=null){
final Route route=MaterialPageRoute(
builder: (context)=>pageContentBuilder(context,arguments:settings.arguments),
);
return route;
}else{
final Route route=MaterialPageRoute(
builder: (context)=>pageContentBuilder(context),
);
return route;
}
},
);
}
}
class HomePageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('hello'),
),
body: new Text('hello'),
floatingActionButton: FloatingActionButton(
onPressed: _pushNewPage,
child: Icon(Icons.add),
),
);
}
void _pushNewPage() {
Navigator.of(context).pushNamed("/page2", arguments: {
"id":110,
});
}
}
class MyPageWidget extends StatelessWidget {
final arguments;
MyPageWidget({this.arguments});
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('MyPageWidget'),
),
body: new Text('${arguments!=null?arguments['id']:'hello'}'),
);
}
}
/*
class MyPageWidget extends StatefulWidget {
final arguments;
MyPageWidget({Key key,this.arguments}):super(key:key);
@override
_MyPageWidgetState createState()=>_MyPageWidgetState(arguments:arguments);
}
class _MyPageWidgetState extends State<MyPageWidget> {
final arguments;
_MyPageWidgetState({this.arguments});
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('MyPageWidget'),
),
body: new Text('${arguments!=null?arguments['id']:'hello'}'),
);
}
}
*/
示例(onGenerateRoute)
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return HelloPageWidget();
})
);
class HelloPageWidget extends StatelessWidget{
final _navigatorKey=GlobalKey<NavigatorState>();
Future bool _onWillPopFunc() async{
final maybePop=await _navigatorKey.currentState.maybePop();
return Future.value(maybePop);
}
@override
Widget build(BuildContext context) {
return WillPopScope( // 使用WillPopScope來使內(nèi)層導(dǎo)航器響應(yīng)Android實體鍵返回。
onWillPop: _onWillPopFunc,
child: Navigator(
key: _navigatorKey,
initialRoute: '/helloPage',
onGenerateRoute: (settings){
WidgetBuilder builder:{
switch(settings.name){ // 路由名
case '/register':
builder=(_)=>RegisterPage();
default:
throw Exception('error');
}
return MaterialPageRoute(
builder: builder,
settings: settings,
);
}
},
),
);
}
}
- fluro路由庫
使用
第1步. 創(chuàng)建FluroRouter路由實例
class RouterManager {
static String splashPath = '/';
static String loginPath = '/login';
static String homePath = '/home';
static String dynamicPath = '/dynamic';
static String dynamicDetailPath = '$dynamicPath/:id';
//
static FluroRouter router;
static void initRouter() {
if (router == null) {
router = FluroRouter();
defineRoutes();
}
}
static var loginHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return LoginPage();
});
static var dynamicDetailHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return DynamicDetailPage(params['id'][0]);
});
static var splashHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return Splash();
});
static var homeHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return AppHomePage();
});
static var notFoundHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return NotFound();
});
// Fluro的路由匹配次序是按照定義路由的先后次序進行匹配的,因此需要把更具體的路由放置在范圍匹配的前面
static void defineRoutes() {
router.define(splashPath, handler: splashHandler);
router.define(homePath, handler: homeHandler);
router.define(loginPath, handler: loginHandler);
router.define(dynamicDetailPath, handler: dynamicDetailHandler);
// 路由不存在時,設(shè)置錯誤路由處理器
router.notFoundHandler = notFoundHandler;
}
}
/*
看一下Handler類的定義:
class Handler {
Handler({this.type = HandlerType.route, required this.handlerFunc});
// route(默認)、function。
final HandlerType type;
// 返回值為將要跳轉(zhuǎn)的頁面。parameters為路由參數(shù)。
// typedef Widget? HandlerFunc(BuildContext? context, Map<String, List<String>> parameters);
final HandlerFunc handlerFunc;
}
*/
第2步. 根Widget中
在build方法開始位置加上:
RouterManager.initRouter();
在MaterialApp中把onGenerateRoute設(shè)置為RouterManager.router.generator。
第3步. 跳轉(zhuǎn)
1. RouterManager.router.navigateTo(context, RouterManager.loginPath);
直接跳轉(zhuǎn)(無參數(shù))
2. RouterManager.router.navigateTo(context, '${RouterManager.dynamicPath}/$id?event=a&event=b')
跳轉(zhuǎn)(帶參數(shù))
3. RouterManager.router.navigateTo(context, RouterManager.homePath, clearStack: true);
清除路由堆棧跳轉(zhuǎn):即跳轉(zhuǎn)后的頁面作為根頁面(沒有返回按鈕)。
設(shè)置轉(zhuǎn)場動畫
enum TransitionType { // 轉(zhuǎn)場動畫類型
native, // 原生形式,和原生的保持一致(默認)
nativeModal, // 原生模態(tài)跳轉(zhuǎn)
inFromLeft, // 從左滑入
inFromTop, // 從頂部滑入
inFromRight, // 從右滑入
inFromBottom,// 從底部滑入
fadeIn, // 漸現(xiàn)
custom, // 自定義,需要配合 transitionBuilder 使用
material, // 安卓風格跳轉(zhuǎn)
materialFullScreenDialog, // 安卓風格全屏對話框(左上角帶有關(guān)閉按鈕)
cupertino, // iOS 風格跳轉(zhuǎn)
cupertinoFullScreenDialog,// iOS風格全屏對話框(左上角帶有關(guān)閉按鈕)
none, // 無轉(zhuǎn)場動畫
}
2種設(shè)置方式
1. 定義路由處理器Handler時設(shè)置transitionType參數(shù)。
router.define(transitionPath,handler: transitionHandler,transitionType: TransitionType.inFromBottom);
2. 使用navigateTo跳轉(zhuǎn)時設(shè)置transition參數(shù)。
RouterManager.router.navigateTo(context, RouterManager.transitionPath,transition: TransitionType.inFromRight,transitionDuration: Duration(milliseconds: 1000));
自定義轉(zhuǎn)場動畫
先看看fluro轉(zhuǎn)場部分源碼是怎么實現(xiàn)轉(zhuǎn)場動畫的:
// 根據(jù)不同的枚舉類型返回不同的動畫。
// TransitionType.fadeIn使用的是Flutter自帶的FadeTransition。上下左右滑入使用的是Flutter自帶的SlideTransition。
RouteTransitionsBuilder _standardTransitionsBuilder(
TransitionType? transitionType) {
return (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
if (transitionType == TransitionType.fadeIn) {
return FadeTransition(opacity: animation, child: child);
} else {
const Offset topLeft = const Offset(0.0, 0.0);
const Offset topRight = const Offset(1.0, 0.0);
const Offset bottomLeft = const Offset(0.0, 1.0);
Offset startOffset = bottomLeft;
Offset endOffset = topLeft;
if (transitionType == TransitionType.inFromLeft) {
startOffset = const Offset(-1.0, 0.0);
endOffset = topLeft;
} else if (transitionType == TransitionType.inFromRight) {
startOffset = topRight;
endOffset = topLeft;
} else if (transitionType == TransitionType.inFromBottom) {
startOffset = bottomLeft;
endOffset = topLeft;
} else if (transitionType == TransitionType.inFromTop) {
startOffset = Offset(0.0, -1.0);
endOffset = topLeft;
}
return SlideTransition(
position: Tween<Offset>(
begin: startOffset,
end: endOffset,
).animate(animation),
child: child,
);
}
};
}
自定義轉(zhuǎn)場動畫只需要transition設(shè)置為TransitionType.custom,然后transitionBuilder返回相應(yīng)動畫就可以了。
/*
Flutter常用轉(zhuǎn)場動畫(繼承自AnimatedWidget)
FadeTransition(透明)
SlideTransition(上下左右滑入)
RotationTransition(旋轉(zhuǎn))
ScaleTransition(縮放)
看一下RotationTransition的定義:
RotationTransition({
Key? key,
// 旋轉(zhuǎn)角度。推薦的起始值0.2至0.3之間,結(jié)束值為0表示回到正常位置。起始值如果為負,則是順時針;如果為正則是逆時針。
required Animation<double> turns,
this.alignment = Alignment.center, // 旋轉(zhuǎn)中心點
this.child,
}) : assert(turns != null),
super(key: key, listenable: turns);
*/
路由攔截
fluro沒有提供類似onGenerateRoute方法來在每次跳轉(zhuǎn)時進行路由攔截。
路由攔截(兩種方式)
1. 在定義路由時,對于未授權(quán)的路由地址跳轉(zhuǎn)到403未授權(quán)頁面。
2. 繼承FluroRouter類,重寫navigateTo跳轉(zhuǎn)方法攔截路由。
示例(自定義轉(zhuǎn)場動畫:逆時針圍繞中心旋轉(zhuǎn))
RouterManager.router.navigateTo(
context,
RouterManager.transitionPath,
transition: TransitionType.custom,
transitionBuilder:
(context, animation, secondaryAnimation, child) {
return RotationTransition(
turns: Tween<double>(
begin: 0.25,
end: 0.0,
).animate(animation),
child: child,
);
},
);
示例(自定義轉(zhuǎn)場動畫:縮放)
RouterManager.router.navigateTo(
context,
RouterManager.transitionPath,
transition: TransitionType.custom,
transitionBuilder:
(context, animation, secondaryAnimation, child) {
return ScaleTransition(
scale: Tween<double>(
begin: 0.5,
end: 1.0,
).animate(animation),
child: child,
);
},
);
示例(自定義轉(zhuǎn)場動畫:通過繼承AnimatedWidget自定義動畫)
class SkewTransition extends AnimatedWidget {
const SkewTransition({
Key key,
Animation<double> turns,
this.alignment = Alignment.center,
this.child,
}) : assert(turns != null),
super(key: key, listenable: turns);
Animation<double> get turns => listenable as Animation<double>;
final Alignment alignment;
final Widget child;
@override
Widget build(BuildContext context) {
final double turnsValue = turns.value;
final Matrix4 transform =
Matrix4.skew(turnsValue * pi * 2.0, turnsValue * pi * 2.0);
// 返回一個 Transform 對象
return Transform(
transform: transform,
alignment: alignment,
child: child,
);
}
}
RouterManager.router.navigateTo(
context,
RouterManager.transitionPath,
transition: TransitionType.custom,
transitionBuilder:
(context, animation, secondaryAnimation, child) {
return SkewTransition(
turns: Tween<double>(
begin: -0.05,
end: 0.0,
).animate(animation),
child: child,
);
},
);
示例(定義路由時攔截)
為了保證路由攔截有效,必須在初始化路由前就通過登錄人信息拿到路由白名單。
為了改善用戶體驗,可以預(yù)先明確哪些頁面不涉及權(quán)限管控(如閃屏頁,首頁,登錄頁)。
// 完整路由表
static final routeTable = {
loginPath: Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return LoginPage();
}),
dynamicDetailPath: Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return DynamicDetailPage(params['id'][0]);
}),
splashPath: Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return Splash();
}),
transitionPath: Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return TransitionPage();
}),
homePath: Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return AppHomePage();
}),
};
// 未授權(quán)頁面處理器
static final permissionDeniedHandler =
Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return PermissionDenied();
});
// 定義路由
// 添加路由時,將路由路徑與白名單進行比對,若不在白名單內(nèi),則使用未授權(quán)路由處理器。
static void defineRoutes({List<String> whiteList}) {
routeTable.forEach((path, handler) {
if (whiteList == null || whiteList.contains(path)) {
router.define(path, handler: handler);
} else {
router.define(path,handler: permissionDeniedHandler,transitionType: TransitionType.material);
}
});
router.notFoundHandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return NotFound();
});
}
示例(繼承FluroRouter類,重寫navigateTo方法攔截路由)
如果首頁不涉及授權(quán),可以在 App 啟動后再獲取授權(quán)白名單,而不需要在啟動時獲取,可以降低啟動時的任務(wù),加快啟動速度和提高用戶體驗。
import 'package:flutter/material.dart';
import 'package:fluro/fluro.dart';
class PermissionRouter extends FluroRouter {
List<String> _whiteList;
set whiteList(value) => _whiteList = value;
String _permissionDeniedPath;
set permissionDeniedPath(value) => _permissionDeniedPath = value;
@override
Future navigateTo(
BuildContext context,
String path, {
bool replace = false,
bool clearStack = false,
bool maintainState = true,
bool rootNavigator = false,
TransitionType transition,
Duration transitionDuration,
transitionBuilder,
RouteSettings routeSettings,
}) {
String pathToNavigate = path;
// 如果匹配成功,則返回匹配的路由對象AppRouteMatch,如果沒有匹配到則返回 null。
AppRouteMatch routeMatched = this.match(path);
// 獲取匹配到的路徑
String routePathMatched = routeMatched?.route?.route;
if (routePathMatched != null) {
// 設(shè)置了白名單且當前路由不在白名單內(nèi),更改路由路徑到授權(quán)被拒絕頁面
if (_whiteList != null && !_whiteList.contains(routePathMatched)) {
pathToNavigate = _permissionDeniedPath;
}
}
return super.navigateTo(context, pathToNavigate,
replace: replace,
clearStack: clearStack,
maintainState: maintainState,
rootNavigator: rootNavigator,
transition: transition,
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
routeSettings: routeSettings);
}
}
/*
match方法匹配成功返回AppRouteMatch對象(有一個AppRoute類型的route屬性)
class AppRoute {
String route; // 匹配到的路由路徑
dynamic handler;
TransitionType? transitionType;
Duration? transitionDuration;
RouteTransitionsBuilder? transitionBuilder;
AppRoute(this.route, this.handler,
{this.transitionType, this.transitionDuration, this.transitionBuilder});
}
*/
- 路由2.0
為了滿足
1. Web端復(fù)雜路由的需要
2. 狀態(tài)驅(qū)動界面的設(shè)計理念。界面與行為進行分離,通過更改狀態(tài)來驅(qū)動界面完成既定行為。
因此,路由2.0最關(guān)鍵的地方就是之前的Navigator.push或Navigator.pop方法不見了,界面只是響應(yīng)用戶操作去更改數(shù)據(jù)狀態(tài),而頁面路由跳轉(zhuǎn)統(tǒng)一交給了RougterDelegate來完成。
優(yōu)點
1. 路由管理和路由解析分離,可以自己定義路由解析類和路由參數(shù)配置類,更為靈活。
2. 路由頁面可以動態(tài)生成,因此實現(xiàn)動態(tài)路由更為簡單。
3. 頁面無需管理跳轉(zhuǎn)邏輯,將頁面和路由分離解耦,保持狀態(tài)驅(qū)動界面的一致性。
4. 可以引入狀態(tài)管理組件來管理整個 App 的路由狀態(tài),擴展性更強。
新加入了如下內(nèi)容:
1. Page
設(shè)置Navigator導(dǎo)航器歷史堆棧的不可變對象。
2. Router
設(shè)置Navigator導(dǎo)航器要顯示的頁面列表。
3. RouteInformationParser
從路由信息提供者(RouteInformationProvider)獲取路由信息并將信息解析轉(zhuǎn)換為用戶定義的數(shù)據(jù)類型。
4. RouterDelegate
定義路由如何獲知App狀態(tài)改變的行為,以及如何響應(yīng)這些行為。它的職責就是監(jiān)聽 RouteInformationParser 和 App 狀態(tài),然后構(gòu)建當前頁面列表的導(dǎo)航器。
5. BackButtonDispatcher
向路由通知返回按鈕點擊事件。
流程
用戶點擊跳轉(zhuǎn),RouteInformationParser解析路徑為對應(yīng)的類,RouterDelegate會調(diào)用其setNewRoutePath方法(傳入解析的類,設(shè)置狀態(tài),調(diào)用notifyListeners),當 notifyListeners 被調(diào)用后會通知Router重建RouterDelegate(調(diào)用build,返回一個新導(dǎo)航器),最新頁面。

示例
1. 在根widget的build方法中使用MaterialApp.router構(gòu)建,設(shè)置路由委托routerDelegate和路由信息解析器routeInformationParser。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: '2.0路由',
routerDelegate: AppRouterDelegate(),
routeInformationParser: AppRouterInformationParser(),
// 省略其他代碼
);
}
}
2. 路由解析類 app_router_path.dart。
import 'package:flutter/cupertino.dart';
// 路由枚舉,不同枚舉對應(yīng)不同頁面
enum RouterPaths { splash, dynamicList, dynamicDetail, notFound }
class AppRouterConfiguration { // 路由配置類
final RouterPaths path; // 當前路由path路徑
final dynamic state; // 狀態(tài)數(shù)據(jù)state(用于將數(shù)據(jù)傳遞到新的頁面)
AppRouterConfiguration(this.path, this.state);
}
// 路由信息解析類,繼承自RouteInformationParser<AppRouterConfiguration>
// 當進行路由跳轉(zhuǎn)時就會調(diào)用路由解析方法,獲取對應(yīng)的路由配置對象。
class AppRouterInformationParser
extends RouteInformationParser<AppRouterConfiguration> {
@override
Future<AppRouterConfiguration> parseRouteInformation(
RouteInformation routeInformation) async {
final String routeName = routeInformation.location;
switch (routeName) {
case '/':
return AppRouterConfiguration(
RouterPaths.splash, routeInformation.state);
case '/home':
return AppRouterConfiguration(
RouterPaths.dynamicList, routeInformation.state);
case '/dynamicDetail':
return AppRouterConfiguration(
RouterPaths.dynamicDetail, routeInformation.state);
default:
return AppRouterConfiguration(
RouterPaths.notFound, routeInformation.state);
}
}
// 不同的路由枚舉返回不同的路由信息對象。
@override
RouteInformation restoreRouteInformation(
AppRouterConfiguration configuration) {
switch (configuration.path) {
case RouterPaths.splash:
return RouteInformation(location: '/', state: configuration.state);
case RouterPaths.dynamicList:
return RouteInformation(location: '/home', state: configuration.state);
case RouterPaths.dynamicDetail:
return RouteInformation(
location: '/dynamicDetail', state: configuration.state);
default:
return RouteInformation(location: '/404', state: configuration.state);
}
}
}
3. 路由委托實現(xiàn)類 router_delegate.dart:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:home_framework/dynamic_detail.dart';
import 'package:home_framework/models/dynamic_entity.dart';
import 'package:home_framework/not_found.dart';
import 'package:home_framework/routers/app_router_path.dart';
import 'package:home_framework/splash.dart';
import '../dynamic.dart';
// AppRouterDelegate繼承自RouterDelegate<AppRouterConfiguration>,實現(xiàn)了ChangeNotifier、PopNavigatorRouterDelegateMixin。
// ChangeNotifier用于增加狀態(tài)更改監(jiān)聽對象(由底層完成)和通知監(jiān)聽對象進行動作,當有狀態(tài)改變時應(yīng)當調(diào)用 notifyListeners方法通知所有監(jiān)聽者。
// PopNavigatorRouterDelegateMixin用于管理返回事件,只有一個方法,可以覆蓋來自定義返回事件。
class AppRouterDelegate extends RouterDelegate<AppRouterConfiguration>
with
ChangeNotifier,
PopNavigatorRouterDelegateMixin<AppRouterConfiguration> {
// 用于存儲導(dǎo)航器狀態(tài)的GlobalKey,可以全局獲知導(dǎo)航器的當前狀態(tài)。
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// 存儲當前路由,發(fā)生改變后會觸發(fā)通知進行路由跳轉(zhuǎn)。
RouterPaths _routerPath;
get routerPath => _routerPath;
set routerPath(RouterPaths value) {
if (_routerPath == value) return;
_routerPath = value;
notifyListeners();
}
// 路由狀態(tài)對象(即路由參數(shù))。
dynamic _state;
get state => _state;
// 啟動頁是否完成,有啟動頁時首頁是啟動頁,用于在啟動完成后將啟動頁移除路由表,以便顯示實際的首頁。
bool _splashFinished = false;
get splashFinished => _splashFinished;
set splashFinished(bool value) {
if (_splashFinished == value) return;
_splashFinished = value;
notifyListeners();
}
// 構(gòu)建路由,通過一個 Navigator 包裹全部路由頁面。
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: _buildPages(),
onPopPage: _handlePopPage,
);
}
// 用于返回 build 方法所需要的pages參數(shù)。
List<Page<void>> _buildPages() {
if (_splashFinished) { // 啟動頁加載完成
return [
MaterialPage(
// 根據(jù)路由枚舉匹配對應(yīng)頁面,同時指定自定義處理方法(該頁面有幾種跳轉(zhuǎn)就需要幾個)。
key: ValueKey('home'), // 第一個路由是首頁。
child: DynamicPage(_handleDynamicItemChanged)), // 對應(yīng)的跳轉(zhuǎn)頁面
if (_routerPath == RouterPaths.splash)
MaterialPage(
key: ValueKey('splash'), child: Splash(_handleSplashFinished)),
if (_routerPath == RouterPaths.dynamicDetail)
MaterialPage(
key: ValueKey('dynamicDetail'), child: DynamicDetail(state)),
if (_routerPath == RouterPaths.notFound)
MaterialPage(key: ValueKey('notFound'), child: NotFound()),
];
} else { //
return [
MaterialPage(
key: ValueKey('splash'), child: Splash(_handleSplashFinished)),
];
}
}
// 一些自定義處理方法
void _handleSplashFinished() {
_routerPath = RouterPaths.dynamicList;
_splashFinished = true;
notifyListeners();
}
void _handleDynamicItemChanged(DynamicEntity dynamicEntity) {
_routerPath = RouterPaths.dynamicDetail;
_state = dynamicEntity;
notifyListeners();
}
// 覆寫了PopNavigatorRouterDelegateMixin的方法
@override
Future<bool> popRoute() async {
return true;
}
// 傳入路由配置對象進行跳轉(zhuǎn)。
@override
Future<void> setNewRoutePath(AppRouterConfiguration configuration) async {
_routerPath = configuration.path;
_state = configuration.state;
}
// 返回處理方法(build方法用到)
bool _handlePopPage(Route<dynamic> route, dynamic result) {
final bool success = route.didPop(result);
return success;
}
// 通過_routerPath和_state構(gòu)建當前的路由配置對象并返回
@override
AppRouterConfiguration get currentConfiguration =>
AppRouterConfiguration(routerPath, state);
}
4. 由于代碼不能再使用 push 和 pop 跳轉(zhuǎn)和返回,因此涉及到這些都需要變更。
啟動頁
class Splash extends StatefulWidget {
final Function onFinished; // 具體實現(xiàn)在AppRouterDelegate類中
Splash(this.onFinished, {Key key}) : super(key: key);
@override
_SplashState createState() => _SplashState(onFinished);
}
class _SplashState extends State<Splash> {
final Function onFinished;
_SplashState(this.onFinished);
bool _initialized = false;
//省略其他代碼
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_initialized) {
_initialized = true;
Timer(const Duration(milliseconds: 2000), () {
onFinished(); // 跳轉(zhuǎn)
});
}
}
}
動態(tài)列表頁
需要接收一個onItemTapped方法來響應(yīng)每行元素的點擊事件,并把點擊的元素作為參數(shù)傳遞。
這種方式暴露了業(yè)務(wù)的實現(xiàn),破壞了封裝性,而且如果父子元素嵌套過深會導(dǎo)致傳遞鏈路過長。
需要使用類似Redux的狀態(tài)管理器來解耦。
2. 資源管理
Flutter APP安裝包中會包含 代碼 和 資源(assets) 兩部分。
常見assets類型:
1. 靜態(tài)數(shù)據(jù)(json文件)
2. 配置文件
3. 圖標和圖片(JPEG,WebP,GIF,動畫WebP / GIF,PNG,BMP和WBMP)
指定assets
和包管理一樣,F(xiàn)lutter使用pubspec.yaml來管理資源。
1. assets指定應(yīng)包含在應(yīng)用程序中的文件。
2. 每個asset都通過相對于pubspec.yaml文件所在的文件系統(tǒng)路徑來標識自身的路徑。
3. asset的聲明順序是無關(guān)緊要的。
4. asset的實際目錄可以是任意文件夾。
5. Asset變體(適配各機型)
在選擇匹配當前設(shè)備分辨率的圖片時,F(xiàn)lutter會使用到asset變體。未來會將這種機制擴展到本地化、閱讀提示等方面。
構(gòu)建過程支持“asset變體”:不同版本的asset可能會顯示在不同的上下文中。 在pubspec.yaml的assets部分中指定asset路徑時,構(gòu)建過程中,會在相鄰子目錄中查找具有相同名稱的任何文件。這些文件隨后會與指定的asset一起被包含在asset bundle中。
6. 在構(gòu)建期間,F(xiàn)lutter將asset放置到稱為asset bundle中,應(yīng)用程序可以在運行時讀取它們(但不能修改)。
示例
flutter:
assets:
- assets/my_icon.png
- assets/background.png
這里的圖片文件夾名是assets,也可是imgs(imgs/my_icon.png)
示例2(Asset變體)
如果應(yīng)用程序目錄中有以下文件:
…/graphics/background.png
…/graphics/dark/background.png
pubspec.yaml文件中只需包含:
flutter:
assets:
- graphics/background.png
graphics/background.png和graphics/dark/background.png 都將會包含在asset bundle中。
前者被認為是main asset (主資源),后者被認為是一種變體(variant)。
- 文本(json文件、)
加載文本(2種方法)
1. 通過rootBundle對象加載
每個Flutter應(yīng)用程序都有一個rootBundle對象, 通過它可以輕松訪問主資源包,直接使用package:flutter/services.dart中全局靜態(tài)的rootBundle對象來加載asset。
2. 通過DefaultAssetBundle加載(建議)
使用DefaultAssetBundle.of(context) 來獲取當前BuildContext的AssetBundle。
獲取的不是應(yīng)用程序構(gòu)建的默認asset bundle,而是使父級widget在運行時動態(tài)替換的不同的AssetBundle,這對于本地化或測試場景很有用。
示例(通過rootBundle對象加載)
通常使用DefaultAssetBundle方式來加載,但在widget上下文之外或其它AssetBundle句柄不可用時,則使用rootBundle加載:
import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('assets/config.json');
}
- 圖片
1. 在pubspec.yaml文件指定asset
AssetImage可以將asset的請求邏輯映射到最接近當前設(shè)備像素比例(dpi)的asset,內(nèi)部會自動處理分辨率。為了使這種映射起作用,必須根據(jù)特定的目錄結(jié)構(gòu)來保存asset:
…/image.png
…/Mx/image.png
…/Nx/image.png
…etc.
其中M和N是數(shù)字標識符,對應(yīng)于其中包含的圖像的分辨率,也就是說,它們指定不同設(shè)備像素比例的圖片。主資源默認對應(yīng)于1.0倍的分辨率圖片。
示例:
…/my_icon.png
…/2.0x/my_icon.png
…/3.0x/my_icon.png
在設(shè)備像素比率為1.8的設(shè)備上,.../2.0x/my_icon.png 將被選擇。對于2.7的設(shè)備像素比率,.../3.0x/my_icon.png將被選擇。
/*
如果未在Image widget上指定渲染圖像的寬度和高度,那么Image widget將占用與主資源相同的屏幕空間大小。即,如果.../my_icon.png是72px乘72px,那么.../3.0x/my_icon.png應(yīng)該是216px乘216px; 但如果未指定寬度和高度,它們都將渲染為72像素×72像素(以邏輯像素為單位)。
pubspec.yaml中asset部分中的每一項都應(yīng)與實際文件相對應(yīng),但主資源項除外。當主資源缺少某個資源時,會按分辨率從低到高的順序去選擇 ,也就是說1x中沒有的話會在2x中找,2x中還沒有的話就在3x中找。
*/
2. 使用AssetImage類加載圖片
Widget build(BuildContext context) {
return new DecoratedBox(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage('graphics/background.png'),
),
),
);
}
AssetImage 并非是一個widget, 它實際上是一個ImageProvider,有些時候可能期望直接得到一個顯示圖片的widget,那么可以使用Image.asset()方法,如:
Widget build(BuildContext context) {
return Image.asset('graphics/background.png');
}
3. 如果要加載依賴包中的圖像,必須給AssetImage提供package參數(shù)。
示例:
假設(shè)應(yīng)用程序依賴于一個名為“my_icons”的包,它具有如下目錄結(jié)構(gòu):
…/pubspec.yaml
…/icons/heart.png
…/icons/1.5x/heart.png
…/icons/2.0x/heart.png
…etc.
然后加載圖像,使用:
new AssetImage('icons/heart.png', package: 'my_icons')
或
new Image.asset('icons/heart.png', package: 'my_icons')
包在使用本身的資源時也必須在pubspec.yaml文件中加上package參數(shù)來獲取。
包也可以選擇在其lib/文件夾中包含未在其pubspec.yaml文件中聲明的資源。例如,一個名為“fancy_backgrounds”的包,可能包含以下文件:
…/lib/backgrounds/background1.png
…/lib/backgrounds/background2.png
…/lib/backgrounds/background3.png
如果要包含第一張圖像,則必須在pubspec.yaml的assets部分中聲明它:
flutter:
assets:
- packages/fancy_backgrounds/backgrounds/background1.png
lib/是隱含的,所以它不應(yīng)該包含在資產(chǎn)路徑中。
上面的資源都是flutter應(yīng)用中的,這些資源只有在Flutter框架運行之后才能使用。
如果要給應(yīng)用設(shè)置APP圖標或者添加啟動圖,那必須使用特定平臺的assets。
特定平臺 assets
iOS
Assets.xcassets中設(shè)置App圖標、啟動圖。直接拖入圖片。
Android
App圖標:.../android/app/src/main/res目錄,里面包含了各種資源文件夾,替換。
啟動圖:.../android/app/src/main目錄。也可以在res/drawable/launch_background.xml,通過自定義drawable來實現(xiàn)自定義啟動界面。
注意:如果重命名了.png文件,則必須在AndroidManifest.xml的<application>標簽的android:icon屬性中更新名稱。
在Flutter框架加載時,F(xiàn)lutter會使用本地平臺機制繪制啟動頁。此啟動頁將持續(xù)到Flutter渲染應(yīng)用程序的第一幀時。這意味著如果不在應(yīng)用程序的main()方法中調(diào)用runApp 函數(shù) (更具體地說,如果不調(diào)用window.render去響應(yīng)window.onDrawFrame)的話, 啟動屏幕將永遠持續(xù)顯示。



- 字體
默認字體
在iOS上:
中文字體:PingFang SC
英文字體:.SF UI Text 、.SF UI Display
在Android 上:
中文字體:Source Han Sans / Noto
英文字體:Roboto
/*
iOS使用.SF的好處:
SF Text 的字距及字母的半封閉空間,比如 "a"! 上半部分會更大,因其可讀性更好,適用于更小的字體;
SF Display 則適用于偏大的字體。
字體小于 20pt 時用 Text ,大于等于 20pt 時用 Display 。
由于 SF 屬于動態(tài)字體,系統(tǒng)自動根據(jù)字體的大小匹配這兩種顯示模式。
*/
自定義字體
1. 在pubspec.yaml中聲明(以確保字體會打包到應(yīng)用程序中)。
flutter:
fonts:
- family: Raleway // family設(shè)置字體名, 用于TextStyle的fontFamily屬性中使用。
fonts:
- asset: assets/fonts/Raleway-Regular.ttf // asset 是相對于 pubspec.yaml 文件的字體路徑
- asset: assets/fonts/Raleway-Medium.ttf
weight: 500 // 指定字體的粗細,取值范圍是100到900之間的整百數(shù)(100的倍數(shù)). 對應(yīng)TextStyle的FontWeight屬性
style: italic // 指定字體是傾斜還是正常,對應(yīng)的值為italic和 normal. 對應(yīng)TextStyle的 fontStyle TextStyle 屬性
- asset: assets/fonts/Raleway-SemiBold.ttf
weight: 600
- family: AbrilFatface
fonts:
- asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf
2. 通過style(TextStyle)使用字體。
// 聲明文本樣式
const textStyle = const TextStyle(
fontFamily: 'Raleway',
);
// 使用文本樣式
var buttonText = const Text(
"Use the font for this text",
style: textStyle,
);
Package中的字體
1. 要使用Package中定義的字體,必須提供package參數(shù)。
如果在package包內(nèi)部使用它自己定義的字體,也應(yīng)該在創(chuàng)建文本樣式時指定package參數(shù)。
const textStyle = const TextStyle(
fontFamily: 'Raleway',
package: 'my_package', // 指定包名
);
2. 一個包也可以只提供字體文件而不需要在pubspec.yaml中聲明。 這些文件應(yīng)該存放在包的lib/文件夾中。字體文件不會自動綁定到應(yīng)用程序中,應(yīng)用程序可以在聲明字體時有選擇地使用這些字體。假設(shè)一個名為my_package的包中有一個字體文件:lib/fonts/Raleway-Medium.ttf
然后,應(yīng)用程序可以聲明一個字體,如下面的示例所示(lib/是隱含的,所以它不應(yīng)該包含在asset路徑中。):
flutter:
fonts:
- family: Raleway
fonts:
- asset: assets/fonts/Raleway-Regular.ttf
- asset: packages/my_package/fonts/Raleway-Medium.ttf
weight: 500
在這種情況下,由于應(yīng)用程序本地定義了字體,所以在創(chuàng)建TextStyle時可以不指定package參數(shù):
const textStyle = const TextStyle(
fontFamily: 'Raleway',
);
示例(自定義字體)
1. 在pubsec.yaml中聲明字體
name: my_application
description: A new Flutter project.
dependencies:
flutter:
sdk: flutter
flutter:
# Include the Material Design fonts.
uses-material-design: true
fonts:
- family: Rock Salt
fonts:
# https://fonts.google.com/specimen/Rock+Salt
- asset: fonts/RockSalt-Regular.ttf
- family: VT323
fonts:
# https://fonts.google.com/specimen/VT323
- asset: fonts/VT323-Regular.ttf
- family: Ewert
fonts:
# https://fonts.google.com/specimen/Ewert
- asset: fonts/Ewert-Regular.ttf
2. 使用字體
import 'package:flutter/material.dart';
const String words1 = "Almost before we knew it, we had left the ground.";
const String words2 = "A shining crescent far beneath the flying vessel.";
const String words3 = "A red flair silhouetted the jagged edge of a wing.";
const String words4 = "Mist enveloped the ship three hours out from port.";
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Fonts',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new FontsPage(),
);
}
}
class FontsPage extends StatefulWidget {
@override
_FontsPageState createState() => new _FontsPageState();
}
class _FontsPageState extends State<FontsPage> {
@override
Widget build(BuildContext context) {
// Rock Salt - https://fonts.google.com/specimen/Rock+Salt
var rockSaltContainer = new Container(
child: new Column(
children: <Widget>[
new Text(
"Rock Salt",
),
new Text(
words2,
textAlign: TextAlign.center,
style: new TextStyle(
fontFamily: "Rock Salt",
fontSize: 17.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
// VT323 - https://fonts.google.com/specimen/VT323
var v2t323Container = new Container(
child: new Column(
children: <Widget>[
new Text(
"VT323",
),
new Text(
words3,
textAlign: TextAlign.center,
style: new TextStyle(
fontFamily: "VT323",
fontSize: 25.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
// https://fonts.google.com/specimen/Ewert
var ewertContainer = new Container(
child: new Column(
children: <Widget>[
new Text(
"Ewert",
),
new Text(
words4,
textAlign: TextAlign.center,
style: new TextStyle(
fontFamily: "Ewert",
fontSize: 25.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
// Material Icons font - included with Material Design
String icons = "";
// https://material.io/icons/#ic_accessible
// accessible:  or 0xE914 or E914
icons += "\u{E914}";
// https://material.io/icons/#ic_error
// error:  or 0xE000 or E000
icons += "\u{E000}";
// https://material.io/icons/#ic_fingerprint
// fingerprint:  or 0xE90D or E90D
icons += "\u{E90D}";
// https://material.io/icons/#ic_camera
// camera:  or 0xE3AF or E3AF
icons += "\u{E3AF}";
// https://material.io/icons/#ic_palette
// palette:  or 0xE40A or E40A
icons += "\u{E40A}";
// https://material.io/icons/#ic_tag_faces
// tag faces:  or 0xE420 or E420
icons += "\u{E420}";
// https://material.io/icons/#ic_directions_bike
// directions bike:  or 0xE52F or E52F
icons += "\u{E52F}";
// https://material.io/icons/#ic_airline_seat_recline_extra
// airline seat recline extra:  or 0xE636 or E636
icons += "\u{E636}";
// https://material.io/icons/#ic_beach_access
// beach access:  or 0xEB3E or EB3E
icons += "\u{EB3E}";
// https://material.io/icons/#ic_public
// public:  or 0xE80B or E80B
icons += "\u{E80B}";
// https://material.io/icons/#ic_star
// star:  or 0xE838 or E838
icons += "\u{E838}";
var materialIconsContainer = new Container(
child: new Column(
children: <Widget>[
new Text(
"Material Icons",
),
new Text(
icons,
textAlign: TextAlign.center,
style: new TextStyle(
inherit: false,
fontFamily: "MaterialIcons",
color: Colors.black,
fontStyle: FontStyle.normal,
fontSize: 25.0,
),
),
],
),
margin: const EdgeInsets.all(10.0),
padding: const EdgeInsets.all(10.0),
decoration: new BoxDecoration(
color: Colors.grey.shade200,
borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
),
);
return new Scaffold(
appBar: new AppBar(
title: new Text("Fonts"),
),
body: new ListView(
children: <Widget>[
rockSaltContainer,
v2t323Container,
ewertContainer,
materialIconsContainer,
],
),
);
}
}
