前言
對于一個產品來說,在業務早期其實更多的是處理基本功能有和無的問題:工程師來負責實現功能,PM 負責功能好用不好用。在產品的基本功能已經完善,做到了六七十分的時候,再往上的如何做增長就需要運營來介入了。
在這其中,如何通過用戶分層去實現 App 的個性化是常見的增長運營手段,而主題樣式更換則是實現個性化中的一項重要技術手段。
比如,微博、UC 瀏覽器和電子書客戶端都提供了對夜間模式的支持,而淘寶、京東這樣的電商類應用,還會在特定的電商活動日自動更新主題樣式,就連現在的手機操作系統也提供了系統級切換展示樣式的能力。
那么,這些在應用內切換樣式的功能是如何實現的呢?在 Flutter 中,在普通的應用上增加切換主題的功能又要做哪些事情呢?
主題定制
主題,又叫皮膚、配色,一般由顏色、圖片、字號、字體等組成,我們可以把它看做是視覺效果在不同場景下的可視資源,以及相應的配置集合。比如,App 的按鈕,無論在什么場景下都需要背景圖片資源、字體顏色、字號大小等,而所謂的主題切換只是在不同主題之間更新這些資源及配置集合而已。
視覺效果是易變的,我們將這些變化的部分抽離處理,把提供不同視覺效果的資源和配置按照主題進行歸類,整合到一個統一的中間層去管理,這樣我們就能實現主題的管理和切換了。
在 Android 中,將配置信息寫入各個 style 屬性值的 xml 中,通過 Activity 的 setTheme 進行切換;前端的處理方式也類似,簡單更換 css 就可以實現多套主題/配色之間的切換。
Flutter 也提供了類似的能力,由 ThemeData 來統一管理主題的配置信息。
ThemeData 涵蓋了 Material Design 規范的可自定義部分樣式,比如應用明暗模式 brightness、應用主色調 primaryColor、應用次級色調 accentColor、文字字體 fontFamliy、輸入框光標顏色 cursorColor 等。
通過 ThemeData 來自定義應用主題,可以實現 App 全局范圍,或是 Widget 局部范圍的樣式切換。接下來,分別對這兩種范圍的主題切換。
(一)全局統一的視覺風格定制
在 Flutter 中,應用程序類 MaterialApp 的初始化方法,為我們提供了設置主題的能力。我們可以通過參數 theme,選擇改變 App 的主題色、字體等,設置界面在 MaterialApp 下的展示樣式。
代碼如下所示:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Theme Data',
theme: ThemeData(
// 明暗模式為暗色
brightness: Brightness.dark,
// 主色調為青色
primaryColor: Colors.cyan,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
運行效果如下:
可以看到,雖然我們只修改了主色調和明暗模式兩個參數,但按鈕、Icon、文字顏色都隨之調整了。這是因為默認情況下,ThemeData 中很多其他次級視覺屬性,都會受到主色調與明暗模式的影響。如果我們想要精確控制它們的展示樣式,需要再細化一下主題配置。
將 Icon 的延伸調整為黃色,文字顏色調整為紅色,按鈕顏色調整為黑色,代碼如下所示:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Theme Data',
theme: ThemeData(
// 明暗模式為暗色
brightness: Brightness.dark,
// 主色調為青色
primaryColor: Colors.cyan,
// 按鈕 Widget 前景色為黑色
accentColor: Colors.black,
// Icon 主題色為黃色
iconTheme: IconThemeData(color: Colors.yellow),
// 設置文本顏色為紅色
textTheme: TextTheme(body1: TextStyle(color: Colors.red)),
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
運行效果如下:
(二)局部獨立的視覺風格定制
為整個 App 提供統一的視覺呈現效果固然很有必要,但有時我們希望為某個頁面、或是某個區塊設置不同于 App 風格的展現樣式。以主題切換功能為例,我們希望為不同的主題提供不同的展示預覽。
在 Flutter 中,我們可以使用 Theme 來對 App 的主題進行局部覆蓋。Theme 是一個單子 Widget 容器,與 MaterialApp 類似的,我們可以通過設置其 data 屬性,對其子 Widget 進行樣式定制:
- 如果我們不想繼承任何 App 全局的顏色或字體樣式,可以直接新建一個 ThemeData 示例,依次設置對應的樣式;
- 而如果不想在局部重寫所有的樣式,則可以繼承 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)),
);
}
}
運行效果如下所示:
對于上述例子而言,由于 Theme 的子 Widget 只有一個 Icon 組件,因此這兩種方式都可以實現覆蓋全局主題,從而更改 Icon 樣式的需求。而像這樣使用局部主題覆蓋全局主題的方式,在 Flutter 中是一種常見的自定義子 Widget 展示樣式的方法。
除了定義 Material Design 規范中那些可自定義部分樣式外,主題的另一個重要用途是樣式復用。
比如,如果我們想為一段文字復用 Materia Design 規范中的 title 樣式,或是為某個子 Widget 的背景色復用 App 的主題色,我們就可以通過 Theme.of(context) 方法,取出對應的屬性,應用到這段文字的樣式中。
Theme.of(context) 方法將向上查找 Widget 樹,并返回 Widget 樹中最近的主題 Theme。如果 Widget 的父 Widget 們有一個單獨的主題定義,則使用該主題。如果不是,那就使用 App 全局主題。
在下面的例子中,我們創建了一個包裝了一個 Text 組件的 Container 容器。在 Text 組件的樣式定義中,我們復用了全局的 title 樣式,而在 Container 的背景色定義中,則復用了 App 的主題色:
Container(
// 容器背景色復用應用主題色
color: Theme.of(context).primaryColor,
child: Text(
'Text with a background color',
//Text 組件文本樣式復用應用文本樣式
style: Theme.of(context).textTheme.title,
));
運行效果如下:
分平臺主題定制
有時候,為了滿足不同平臺的用戶需求,我們希望針對特定的平臺設置不同的樣式。比如,在 iOS 平臺上設置淺色主題,在 Android 平臺上設置深色主題。面對這樣的需求,我們可以根據 defaultTargetPlatform 來判斷當前應用所運行的平臺,從而根據系統類型來設置對應的主題。
在下面的例子中,我們為 iOS 與 Android 分別創建了兩個主題。在 MaterialApp 的初始化方法中,我們根據平臺類型,設置了不同的主題:
// 使用 defaultTargetPlatform 需要導入
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,
// 主題色為藍色
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,
// 主題色為藍色
primaryColor: Colors.cyan,
// Icon 主題為灰色
iconTheme: IconThemeData(color: Colors.blue),
// 文本主題為黑色
textTheme: TextTheme(body1: TextStyle(color: Colors.red)),
);
return MaterialApp(
title: 'Flutter Theme Data',
// 判斷手機類型,設置主題樣式
theme:
defaultTargetPlatform == TargetPlatform.iOS ? iOsTheme : androidTheme,
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
分別運行效果如下(沒有 iOS 手機,用安卓代替一下):
當然,除了主題之外,也可以用 defaultTargetPlatform 這個變量去實現一些其他需要判斷平臺的邏輯,比如在界面上使用更符合 Android 或 iOS 設計風格的組件。