Flutter了解之入門篇3(路由管理、資源管理)

目錄
  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();
           }));
          },
         ),
       ],
 )
  1. 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
  1. 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)。
  1. 路由傳值(跳轉(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("返回"),
              )
            ],
          ),
        ),
      ),
    );
  }
}
  1. 命名路由(有名字的路由)

給路由起名字后,可以通過路由名來打開新路由。

通過路由名打開新路由時,應(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,
            );
          }
        },
      ),
    );
  }
}
  1. 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});
}
*/
  1. 路由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)航器),最新頁面。
路由2.0

示例

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)。
  1. 文本(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. 圖片
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

iOS

Android
  1. 字體

默認字體

在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: &#xE914; or 0xE914 or E914
    icons += "\u{E914}";
    // https://material.io/icons/#ic_error
    // error: &#xE000; or 0xE000 or E000
    icons += "\u{E000}";
    // https://material.io/icons/#ic_fingerprint
    // fingerprint: &#xE90D; or 0xE90D or E90D
    icons += "\u{E90D}";
    // https://material.io/icons/#ic_camera
    // camera: &#xE3AF; or 0xE3AF or E3AF
    icons += "\u{E3AF}";
    // https://material.io/icons/#ic_palette
    // palette: &#xE40A; or 0xE40A or E40A
    icons += "\u{E40A}";
    // https://material.io/icons/#ic_tag_faces
    // tag faces: &#xE420; or 0xE420 or E420
    icons += "\u{E420}";
    // https://material.io/icons/#ic_directions_bike
    // directions bike: &#xE52F; or 0xE52F or E52F
    icons += "\u{E52F}";
    // https://material.io/icons/#ic_airline_seat_recline_extra
    // airline seat recline extra: &#xE636; or 0xE636 or E636
    icons += "\u{E636}";
    // https://material.io/icons/#ic_beach_access
    // beach access: &#xEB3E; or 0xEB3E or EB3E
    icons += "\u{EB3E}";
    // https://material.io/icons/#ic_public
    // public: &#xE80B; or 0xE80B or E80B
    icons += "\u{E80B}";
    // https://material.io/icons/#ic_star
    // star: &#xE838; 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,
        ],
      ),
    );
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容