Flutter 定制 App 的夜間模式

前言

對于一個產品來說,在業務早期其實更多的是處理基本功能有和無的問題:工程師來負責實現功能,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'),
    );
  }
}

運行效果如下:


Flutter 全局模式主題

可以看到,雖然我們只修改了主色調和明暗模式兩個參數,但按鈕、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'),
    );
  }
}

運行效果如下:


Flutter 全局模式主題 2

(二)局部獨立的視覺風格定制

為整個 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 局部主題更改示例

對于上述例子而言,由于 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 手機,用安卓代替一下):

iOS 平臺

Android 平臺

當然,除了主題之外,也可以用 defaultTargetPlatform 這個變量去實現一些其他需要判斷平臺的邏輯,比如在界面上使用更符合 Android 或 iOS 設計風格的組件。

ThemeData 官方文檔

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容

  • 前言 上一篇我們簡單地了解了 Dart 語言,接著我們就開始學習 Flutter 的基礎 Widget 吧。 1....
    南小夕閱讀 2,595評論 0 8
  • 在Flutter中使用ThemeData來在應用中共享顏色和字體樣式,Theme有兩種:全局Theme和局部The...
    Magician閱讀 21,867評論 7 34
  • 原文在此,此處只為學習 Widget與ElementWidget主要接口Stateless WidgetState...
    lltree閱讀 4,524評論 0 1
  • 那天我下載好了游戲滿懷著激動看了角色我選擇了明教,但是對于一個小白,游戲小白來說簡直是給自己挖了一個坑。我多次在茫...
    離珩閱讀 195評論 0 0
  • 文/若凡 “媽媽,別走...” 寧心又一次從噩夢中醒來,那個背影還清晰的留在腦海里。 她想起夢里那個人,似乎還能感...
    璃若凡閱讀 344評論 12 16