Flutter 自定义主题风格

质感设计的Theme类将主题应用于后代控件,主题描述了应用程序的颜色和排版选择。后代控件使用Theme.of获取当前主题的ThemeData对象,当控件使用Theme.of时,如果主题稍后更改,则会自动重建,以便可以应用更改。我们可以通过Theme.of查看当前应用程序的配色方案。

class _MyHomePageState extends State<MyHomePage> {
  Widget _colorDisplayBox(String explanation, String name, Color color) {
    return new Column(
      children: <Widget>[
        new Row(
          children: <Widget>[
            new Text("$explanation\n$name\t\t"),
            new Flexible(
                child: new Container(
                    height: 30.0, decoration: new BoxDecoration(color: color)))
        ],),
        new Divider()
  ],);}

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      //...
      body: new Center(
        child: new ListView(
          padding: new EdgeInsets.all(8.0),
          children: <Widget>[
            _colorDisplayBox("突出颜色", "highlightColor", Theme.of(context).highlightColor),
            _colorDisplayBox("提示颜色", "hintColor", Theme.of(context).hintColor),
            _colorDisplayBox("文本选择手柄颜色", "textSelectionHandleColor", Theme.of(context).textSelectionHandleColor),
            _colorDisplayBox("文字选择颜色", "textSelectionColor", Theme.of(context).textSelectionColor),
            _colorDisplayBox("背景颜色", "backgroundColor", Theme.of(context).backgroundColor),
            _colorDisplayBox("强调颜色", "accentColor", Theme.of(context).accentColor),
            _colorDisplayBox("画布颜色", "canvasColor", Theme.of(context).canvasColor),
            _colorDisplayBox("卡片颜色", "cardColor", Theme.of(context).cardColor),
            _colorDisplayBox("按钮颜色", "buttonColor", Theme.of(context).buttonColor),
            _colorDisplayBox("对话框背景颜色", "dialogBackgroundColor", Theme.of(context).dialogBackgroundColor),
            _colorDisplayBox("禁用颜色", "disabledColor", Theme.of(context).disabledColor),
            _colorDisplayBox("分频器颜色", "dividerColor", Theme.of(context).dividerColor),
            _colorDisplayBox("错误颜色", "errorColor", Theme.of(context).errorColor),
            _colorDisplayBox("指示灯颜色", "indicatorColor", Theme.of(context).indicatorColor),
            _colorDisplayBox("原色", "primaryColor", Theme.of(context).primaryColor),
            _colorDisplayBox("脚手架背景颜色", "scaffoldBackgroundColor", Theme.of(context).scaffoldBackgroundColor),
            _colorDisplayBox("次标头颜色", "secondaryHeaderColor", Theme.of(context).secondaryHeaderColor),
            _colorDisplayBox("选择行颜色", "selectedRowColor", Theme.of(context).selectedRowColor),
            _colorDisplayBox("飞溅颜色", "splashColor", Theme.of(context).splashColor),
            _colorDisplayBox("未选择的控件颜色", "unselectedWidgetColor", Theme.of(context).unselectedWidgetColor),
],),),);}}
image.png

我们可以自定义配色,比如设置primaryColor为红色。还可以使用primaryColorBrightness设置primaryColor的亮度,用于设置放置在原色顶部的文本和图标的颜色。

需要注意这里的亮度是描述颜色的对比度需求,有两个常量。Brightness.dark表示颜色很暗,需要浅色文字颜色才能实现可读性,例如颜色可能是深灰色,需要白色文字。Brightness.light表示颜色很浅,需要深色文字颜色来实现可读性,例如颜色可能是明亮的白色,需要黑色文字。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      theme: new ThemeData(
        primaryColor: Colors.red,
        primaryColorBrightness: Brightness.dark,
      ),
      title: 'Flutter Demo',
      home: new MyHomePage(),
    );
  }
}
image.png

我们还可以根据不同平台设置主题,比如我们在iOS上设置白色和灰色主题,在Android上设置紫色和橙色主题。判断平台类型需要使用defaultTargetPlatform来识别当前系统平台,然后根据平台类型设置主题。

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';

void main() {
  runApp(new MyApp());
}

final ThemeData kIOSTheme = new ThemeData(
  primarySwatch: Colors.orange,
  primaryColor: Colors.grey[100],
  primaryColorBrightness: Brightness.light,
);

final ThemeData kDefaultTheme = new ThemeData(
  primarySwatch: Colors.purple,
  accentColor: Colors.orangeAccent[400],
);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      theme: defaultTargetPlatform == TargetPlatform.iOS
          ? kIOSTheme
          : kDefaultTheme,

      title: 'Flutter Demo',
      home: new MyHomePage(),
    );
  }
}
image.png

在上面的代码中,有一段大家可能有疑问:primarySwatch: Colors.orange。颜色(Color)是ARGB格式的不可变的32位颜色值,具有相关颜色的小表的颜色称为样本(Swatch),颜色和样本的常量,表示质感设计的调色板。

image.png

大多数色板的颜色从100到900,增量为100,加上颜色50。数字越小,颜色越浅,数字越大,颜色越暗。强调色调(例如redAccent)只有数值100、200、400和700。

image.png

此外,还有一系列具有普遍不透明度的黑色和白色。例如,black54是具有54%不透明度的纯黑色。

image.png

要从其中一个色板中选择特定颜色,需要使用所需特定颜色的整数索引到样本。每个颜色样本常量是一种颜色,可以直接使用。

Color selection = Colors.green[400];

new Container(
  color: Colors.blue,
)

我们都知道在 Flutter 中可以通过 fontFamily 来引入第三方字体,例如通常会将 svg 图标转换为 **iconfont.ttf** 来实现矢量图标的入,而一般情况下我们是不会设置 fontFamily 来使用第三方字体, 那默认情况下 Flutter 使用的是什么字体呢?

会出现这个疑问,是因为有一天设计给我发了下面那张图,问我 “为什么应用在苹果平台上的英文使用的是 PingFang SC 字体而不是 .SF UI Display ? 正如下图所示,它们的 G 字母在显示效果上会有所差异,比如 平方的 G 有明显的转折线。

image.png

这时候我不禁产生的好奇,在 Flutter 中引擎默认究竟是如何选择字体?

通过官方解释,在 typography.dart 源码中可以看到,

  • Flutter 默认在 Android 上使用的是 Roboto 字体;
  • 在 iOS 上使用的是 .SF UI Display 或者 .SF UI Text 字体。

The default font on Android is Roboto and on iOS it is .SF UI Display or .SF UI Text (SF meaning San Francisco). If you want to use a different font, then you will need to add it to your app.

image.png

那理论上在 iOS 使用的就是 .SF UI Display 字体才对,因为如下源码所示,在 Typography 中当 platformiOS 时,使用的就是 Cupertino 相关的 TextTheme,而 Typography 中的 whiteblack 属性最终会应用到 ThemeDatadefaultTextThemedefaultPrimaryTextThemedefaultAccentTextTheme 中,所以应该是使用 .SF 相关字体才会,为什么会显示的是 PingFang SC 的效果?

factory Typography({
    TargetPlatform platform = TargetPlatform.android,
    TextTheme black,
    TextTheme white,
    TextTheme englishLike,
    TextTheme dense,
    TextTheme tall,
  }) {
    assert(platform != null || (black != null && white != null));
    switch (platform) {
      case TargetPlatform.iOS:
        black ??= blackCupertino;
        white ??= whiteCupertino;
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        black ??= blackMountainView;
        white ??= whiteMountainView;
    }
    englishLike ??= englishLike2014;
    dense ??= dense2014;
    tall ??= tall2014;
    return Typography._(black, white, englishLike, dense, tall);
  }

为了搞清不同系统上字体的区别,在查阅了资料后可知:

  • 默认在 iOS 上:
    • 中文字体:PingFang SC
    • 英文字体:.SF UI Text.SF UI Display
  • 默认在 Android 上:
    • 中文字体:Source Han Sans / Noto
    • 英文字体:Roboto

也就是就 iOS 上除了 .SF 相关的字体外,还有 PingFang 字体的存在,这时候我突然想起在之前的 《Flutter完整开发实战详解(十七、 实用技巧与填坑二)》 中,因为国际化多语言在 .SF 会出现显示异常,所以使用了 fontFamilyFallback 强行指定了 PingFang SC

getCopyTextStyle(TextStyle textStyle) {
    return textStyle.copyWith(fontFamilyFallback: ["PingFang SC", "Heiti SC"]);
  }
image.png

终于破案了,因为当 fontFamily 没有设置时,就会使用 fontFamilyFallback 中的第一个值将作为首选字体,而在 fontFamilyFallback 中是顺序匹配的,当fontFamilyfontFamilyFallback 两者都不提供,则将使用默认平台字体。

而在 1.12.13 版本下测试发现 .SF 导致的问题已经修复了,所以只需要将 fontFamilyFallback 相关的代码去除即可。

那在 iOS 上使用 .SF 字体有什么好处? 按照网络上的说法是:

SF Text 的字距及字母的半封闭空间,比如 "a"! 上半部分会更大,因其可读性更好,适用于更小的字体; SF Display 则适用于偏大的字体。具体分水岭就是 20pt , 即字体小于 20pt 时用 Text ,大于等于 20pt 时用 Display 。 更棒的是由于 SF 属于动态字体,TextDisplay 两种字体族是系统动态匹配的,也就是说你不用费心去自己手动调节,系统自动根据字体的大小匹配这两种显示模式。

那能不能在 Android 上也使用.SF 字体呢?按照官方的说法:

  • 在使用 Material package 时,在 Android 上使用的是 ·Roboto font· ,而 iOS 使用的是 San Francisco font(SF)
  • 在使用 Cupertino package 时,默认主题始终使用 San Francisco font(SF)

但是因为 San Francisco font license 限制了该字体只能在 iOS、macOS 或 tvOS 上运行使用,所以如果使用了 Cupertino 主题的话,在 Android 上运行时使用 fallback font。

所以你觉得能不能在 Android 上使用?

最后再补充下,在官方的 architecture 中有提到,在 Flutter 中的文本呈现逻辑是有分层的,其中:

  • 衍生自 Minikin 的 libtxt 库用于字体选择,分隔行等;
  • HartBuzz 用于字形选择和成型;
  • Skia作为 渲染 / GPU后端;
  • 在 Android / Fuchsia 上使用 FreeType 渲染,在 iOS 上使用CoreGraphics 来渲染字体 。
image.png

在 Flutter 中,是没有直接设置 Text 行间距的方法的, Text 显示的效果是如下图所示的逻辑组成:

image.png

那么我们应该如何处理行间距呢?如下图所示,通过设置 StrutStyleleading , 然后利用 Transform 做计算翻方向位置偏移,因为 leading 是上下均衡的,所以计算后就可以得到我们所需要的行间距大小。 (虽然无法保证一定 100%像素准确,你是否还知道其他方法?)

image.png

这里额外提一点,可以通过父节点使用 DefaultTextStyle 来实现局部样式的共享哦。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容