前言
對(duì)于一個(gè)產(chǎn)品來(lái)說(shuō),在業(yè)務(wù)早期其實(shí)更多的是處理基本功能有和無(wú)的問(wèn)題:工程師來(lái)負(fù)責(zé)實(shí)現(xiàn)功能抛寝,PM 負(fù)責(zé)功能好用不好用曙旭。在產(chǎn)品的基本功能已經(jīng)完善晶府,做到了六七十分的時(shí)候桂躏,再往上的如何做增長(zhǎng)就需要運(yùn)營(yíng)來(lái)介入了川陆。
在這其中剂习,如何通過(guò)用戶(hù)分層去實(shí)現(xiàn) App 的個(gè)性化是常見(jiàn)的增長(zhǎng)運(yùn)營(yíng)手段较沪,而主題樣式更換則是實(shí)現(xiàn)個(gè)性化中的一項(xiàng)重要技術(shù)手段鳞绕。
比如尸曼,微博们何、UC 瀏覽器和電子書(shū)客戶(hù)端都提供了對(duì)夜間模式的支持控轿,而淘寶冤竹、京東這樣的電商類(lèi)應(yīng)用茬射,還會(huì)在特定的電商活動(dòng)日自動(dòng)更新主題樣式鹦蠕,就連現(xiàn)在的手機(jī)操作系統(tǒng)也提供了系統(tǒng)級(jí)切換展示樣式的能力。
那么躲株,這些在應(yīng)用內(nèi)切換樣式的功能是如何實(shí)現(xiàn)的呢片部?在 Flutter 中,在普通的應(yīng)用上增加切換主題的功能又要做哪些事情呢霜定?
主題定制
主題档悠,又叫皮膚、配色望浩,一般由顏色辖所、圖片磨德、字號(hào)、字體等組成典挑,我們可以把它看做是視覺(jué)效果在不同場(chǎng)景下的可視資源,以及相應(yīng)的配置集合您觉。比如拙寡,App 的按鈕琳水,無(wú)論在什么場(chǎng)景下都需要背景圖片資源般堆、字體顏色、字號(hào)大小等诚啃,而所謂的主題切換只是在不同主題之間更新這些資源及配置集合而已淮摔。
視覺(jué)效果是易變的,我們將這些變化的部分抽離處理始赎,把提供不同視覺(jué)效果的資源和配置按照主題進(jìn)行歸類(lèi)和橙,整合到一個(gè)統(tǒng)一的中間層去管理极阅,這樣我們就能實(shí)現(xiàn)主題的管理和切換了胃碾。
在 Android 中筋搏,將配置信息寫(xiě)入各個(gè) style 屬性值的 xml 中仆百,通過(guò) Activity 的 setTheme 進(jìn)行切換奔脐;前端的處理方式也類(lèi)似,簡(jiǎn)單更換 css 就可以實(shí)現(xiàn)多套主題/配色之間的切換髓迎。
Flutter 也提供了類(lèi)似的能力,由 ThemeData 來(lái)統(tǒng)一管理主題的配置信息排龄。
ThemeData 涵蓋了 Material Design 規(guī)范的可自定義部分樣式波势,比如應(yīng)用明暗模式 brightness橄维、應(yīng)用主色調(diào) primaryColor尺铣、應(yīng)用次級(jí)色調(diào) accentColor争舞、文字字體 fontFamliy凛忿、輸入框光標(biāo)顏色 cursorColor 等竞川。
通過(guò) ThemeData 來(lái)自定義應(yīng)用主題,可以實(shí)現(xiàn) App 全局范圍委乌,或是 Widget 局部范圍的樣式切換。接下來(lái)遭贸,分別對(duì)這兩種范圍的主題切換叠赦。
(一)全局統(tǒng)一的視覺(jué)風(fēng)格定制
在 Flutter 中,應(yīng)用程序類(lèi) MaterialApp 的初始化方法除秀,為我們提供了設(shè)置主題的能力。我們可以通過(guò)參數(shù) theme册踩,選擇改變 App 的主題色、字體等暂吉,設(shè)置界面在 MaterialApp 下的展示樣式。
代碼如下所示:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Theme Data',
theme: ThemeData(
// 明暗模式為暗色
brightness: Brightness.dark,
// 主色調(diào)為青色
primaryColor: Colors.cyan,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
運(yùn)行效果如下:
可以看到慕的,雖然我們只修改了主色調(diào)和明暗模式兩個(gè)參數(shù)挤渔,但按鈕肮街、Icon判导、文字顏色都隨之調(diào)整了。這是因?yàn)槟J(rèn)情況下眼刃,ThemeData 中很多其他次級(jí)視覺(jué)屬性,都會(huì)受到主色調(diào)與明暗模式的影響擂红。如果我們想要精確控制它們的展示樣式仪际,需要再細(xì)化一下主題配置昵骤。
將 Icon 的延伸調(diào)整為黃色,文字顏色調(diào)整為紅色涉茧,按鈕顏色調(diào)整為黑色,代碼如下所示:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Theme Data',
theme: ThemeData(
// 明暗模式為暗色
brightness: Brightness.dark,
// 主色調(diào)為青色
primaryColor: Colors.cyan,
// 按鈕 Widget 前景色為黑色
accentColor: Colors.black,
// Icon 主題色為黃色
iconTheme: IconThemeData(color: Colors.yellow),
// 設(shè)置文本顏色為紅色
textTheme: TextTheme(body1: TextStyle(color: Colors.red)),
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
運(yùn)行效果如下:
(二)局部獨(dú)立的視覺(jué)風(fēng)格定制
為整個(gè) App 提供統(tǒng)一的視覺(jué)呈現(xiàn)效果固然很有必要伦连,但有時(shí)我們希望為某個(gè)頁(yè)面、或是某個(gè)區(qū)塊設(shè)置不同于 App 風(fēng)格的展現(xiàn)樣式惑淳。以主題切換功能為例,我們希望為不同的主題提供不同的展示預(yù)覽歧焦。
在 Flutter 中,我們可以使用 Theme 來(lái)對(duì) App 的主題進(jìn)行局部覆蓋绢馍。Theme 是一個(gè)單子 Widget 容器向瓷,與 MaterialApp 類(lèi)似的舰涌,我們可以通過(guò)設(shè)置其 data 屬性,對(duì)其子 Widget 進(jìn)行樣式定制:
- 如果我們不想繼承任何 App 全局的顏色或字體樣式瓷耙,可以直接新建一個(gè) ThemeData 示例,依次設(shè)置對(duì)應(yīng)的樣式搁痛;
- 而如果不想在局部重寫(xiě)所有的樣式长搀,則可以繼承 App 的主題鸡典,使用 copyWith 方法,只更新部分樣式轿钠。
代碼如下所示:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
Theme(
data: ThemeData(iconTheme: IconThemeData(color: Colors.red)),
child: Icon(Icons.favorite));
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Icon(Icons.favorite),
Text('Flutter Theme Data'),
],
mainAxisAlignment: MainAxisAlignment.center,
),
Row(
children: <Widget>[
// 新建主題
Theme(
data: ThemeData(iconTheme: IconThemeData(color: Colors.red)),
child: Icon(Icons.favorite)),
Text('Flutter Theme Data'),
],
mainAxisAlignment: MainAxisAlignment.center,
),
Row(
children: <Widget>[
// 繼承主題
Theme(
data: Theme.of(context)
.copyWith(iconTheme: IconThemeData(color: Colors.green)),
child: Icon(Icons.favorite)),
Text('Flutter Theme Data'),
],
mainAxisAlignment: MainAxisAlignment.center,
)
],
)),
floatingActionButton:
FloatingActionButton(onPressed: null, child: Icon(Icons.add)),
);
}
}
運(yùn)行效果如下所示:
對(duì)于上述例子而言,由于 Theme 的子 Widget 只有一個(gè) Icon 組件症汹,因此這兩種方式都可以實(shí)現(xiàn)覆蓋全局主題,從而更改 Icon 樣式的需求背镇。而像這樣使用局部主題覆蓋全局主題的方式,在 Flutter 中是一種常見(jiàn)的自定義子 Widget 展示樣式的方法瞒斩。
除了定義 Material Design 規(guī)范中那些可自定義部分樣式外,主題的另一個(gè)重要用途是樣式復(fù)用胸囱。
比如,如果我們想為一段文字復(fù)用 Materia Design 規(guī)范中的 title 樣式烹笔,或是為某個(gè)子 Widget 的背景色復(fù)用 App 的主題色,我們就可以通過(guò) Theme.of(context) 方法谤职,取出對(duì)應(yīng)的屬性,應(yīng)用到這段文字的樣式中允蜈。
Theme.of(context) 方法將向上查找 Widget 樹(shù),并返回 Widget 樹(shù)中最近的主題 Theme饶套。如果 Widget 的父 Widget 們有一個(gè)單獨(dú)的主題定義漩蟆,則使用該主題凤跑。如果不是,那就使用 App 全局主題仔引。
在下面的例子中,我們創(chuàng)建了一個(gè)包裝了一個(gè) Text 組件的 Container 容器咖耘。在 Text 組件的樣式定義中,我們復(fù)用了全局的 title 樣式儿倒,而在 Container 的背景色定義中,則復(fù)用了 App 的主題色:
Container(
// 容器背景色復(fù)用應(yīng)用主題色
color: Theme.of(context).primaryColor,
child: Text(
'Text with a background color',
//Text 組件文本樣式復(fù)用應(yīng)用文本樣式
style: Theme.of(context).textTheme.title,
));
運(yùn)行效果如下:
分平臺(tái)主題定制
有時(shí)候夫否,為了滿足不同平臺(tái)的用戶(hù)需求叫胁,我們希望針對(duì)特定的平臺(tái)設(shè)置不同的樣式凰慈。比如驼鹅,在 iOS 平臺(tái)上設(shè)置淺色主題微谓,在 Android 平臺(tái)上設(shè)置深色主題输钩。面對(duì)這樣的需求,我們可以根據(jù) defaultTargetPlatform 來(lái)判斷當(dāng)前應(yīng)用所運(yùn)行的平臺(tái)买乃,從而根據(jù)系統(tǒng)類(lèi)型來(lái)設(shè)置對(duì)應(yīng)的主題。
在下面的例子中剪验,我們?yōu)?iOS 與 Android 分別創(chuàng)建了兩個(gè)主題。在 MaterialApp 的初始化方法中碉咆,我們根據(jù)平臺(tái)類(lèi)型,設(shè)置了不同的主題:
// 使用 defaultTargetPlatform 需要導(dǎo)入
import 'package:flutter/foundation.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// iOS 淺色主題
final ThemeData iOsTheme = ThemeData(
// 亮色主題
brightness: Brightness.light,
// 按鈕前景色為白色
accentColor: Colors.white,
// 主題色為藍(lán)色
primaryColor: Colors.blue,
// Icon 主題為灰色
iconTheme: IconThemeData(color: Colors.grey),
// 文本主題為黑色
textTheme: TextTheme(body1: TextStyle(color: Colors.black)),
);
// Android 深色主題
final ThemeData androidTheme = ThemeData(
// 亮色主題
brightness: Brightness.dark,
// 按鈕前景色為白色
accentColor: Colors.black,
// 主題色為藍(lán)色
primaryColor: Colors.cyan,
// Icon 主題為灰色
iconTheme: IconThemeData(color: Colors.blue),
// 文本主題為黑色
textTheme: TextTheme(body1: TextStyle(color: Colors.red)),
);
return MaterialApp(
title: 'Flutter Theme Data',
// 判斷手機(jī)類(lèi)型疫铜,設(shè)置主題樣式
theme:
defaultTargetPlatform == TargetPlatform.iOS ? iOsTheme : androidTheme,
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
分別運(yùn)行效果如下(沒(méi)有 iOS 手機(jī),用安卓代替一下):
當(dāng)然席揽,除了主題之外顽馋,也可以用 defaultTargetPlatform 這個(gè)變量去實(shí)現(xiàn)一些其他需要判斷平臺(tái)的邏輯幌羞,比如在界面上使用更符合 Android 或 iOS 設(shè)計(jì)風(fēng)格的組件。