Flutter 定制 App 的夜間模式

前言

對(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)行效果如下:


Flutter 全局模式主題

可以看到慕的,雖然我們只修改了主色調(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)行效果如下:


Flutter 全局模式主題 2

(二)局部獨(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)行效果如下所示:


Theme 局部主題更改示例

對(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)行效果如下:


主題復(fù)用

分平臺(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ī),用安卓代替一下):

iOS 平臺(tái)

Android 平臺(tái)

當(dāng)然席揽,除了主題之外顽馋,也可以用 defaultTargetPlatform 這個(gè)變量去實(shí)現(xiàn)一些其他需要判斷平臺(tái)的邏輯幌羞,比如在界面上使用更符合 Android 或 iOS 設(shè)計(jì)風(fēng)格的組件。

ThemeData 官方文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末属桦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子聂宾,更是在濱河造成了極大的恐慌逝撬,老刑警劉巖镜会,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異纪他,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)茶袒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)弹谁,“玉大人,你說(shuō)我怎么就攤上這事预愤。” “怎么了植康?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)销睁。 經(jīng)常有香客問(wèn)我,道長(zhǎng)冻记,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任冗栗,我火速辦了婚禮供搀,結(jié)果婚禮上钠至,老公的妹妹穿的比我還像新娘葛虐。我一直安慰自己棉钧,他們只是感情好屿脐,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布宪卿。 她就那樣靜靜地躺著,像睡著了一般佑钾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上次绘,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天撒遣,我揣著相機(jī)與錄音邮偎,去河邊找鬼义黎。 笑死禾进,一個(gè)胖子當(dāng)著我的面吹牛廉涕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播狐蜕,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼层释!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起贡羔,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎乖寒,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體楣嘁,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡珍逸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了弄息。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡摹量,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缨称,到底是詐尸還是另有隱情,我是刑警寧澤祝迂,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站当凡,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏纠俭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一冤荆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钓简,春花似錦、人聲如沸外邓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至席镀,卻和暖如春匹中,著一層夾襖步出監(jiān)牢的瞬間豪诲,已是汗流浹背顶捷。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工屎篱, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留葵蒂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓践付,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親永高。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 前言 上一篇我們簡(jiǎn)單地了解了 Dart 語(yǔ)言命爬,接著我們就開(kāi)始學(xué)習(xí) Flutter 的基礎(chǔ) Widget 吧。 1....
    南小夕閱讀 2,556評(píng)論 0 8
  • 在Flutter中使用ThemeData來(lái)在應(yīng)用中共享顏色和字體樣式饲宛,Theme有兩種:全局Theme和局部The...
    Magician閱讀 21,839評(píng)論 7 34
  • 原文在此嗜价,此處只為學(xué)習(xí) Widget與ElementWidget主要接口Stateless WidgetState...
    lltree閱讀 4,501評(píng)論 0 1
  • 那天我下載好了游戲滿懷著激動(dòng)看了角色我選擇了明教艇抠,但是對(duì)于一個(gè)小白久锥,游戲小白來(lái)說(shuō)簡(jiǎn)直是給自己挖了一個(gè)坑练链。我多次在茫...
    離珩閱讀 191評(píng)論 0 0
  • 文/若凡 “媽媽?zhuān)瑒e走...” 寧心又一次從噩夢(mèng)中醒來(lái)奴拦,那個(gè)背影還清晰的留在腦海里届吁。 她想起夢(mèng)里那個(gè)人错妖,似乎還能感...
    璃若凡閱讀 331評(píng)論 12 16