Flutter-主题

49 阅读3分钟

ThemeData

ThemeData 用于保存是Material组件库的主题数据,Material组件需要遵守相应的设计规范,这些规范可自定义部分都定义在ThemeData中,所以可以通过ThemeData来定义应用主题。在子组件中,可以通过Theme.of来获取当前的ThemeData。

注意:Material Design设计规范中有些是不能自定义的,如导航栏高度,ThemeData只包含了可自定义部分。

常用的属性:

ThemeData({
  Brightness? brightness, //深色还是浅色
  MaterialColor? primarySwatch, //主题颜色样本,见下面介绍
  Color? primaryColor, //主色,决定导航栏颜色
  Color? cardColor, //卡片颜色
  Color? dividerColor, //分割线颜色
  ButtonThemeData buttonTheme, //按钮主题
  Color dialogBackgroundColor,//对话框背景颜色
  String fontFamily, //文字字体
  TextTheme textTheme,// 字体主题,包括标题、body等文字样式
  IconThemeData iconTheme, // Icon的默认样式
  TargetPlatform platform, //指定平台,应用特定平台控件风格
  ColorScheme? colorScheme,
  ...
})
  • PrimarySwatch 是主题颜色的一个样本色,通过这个样本色,可以在一些条件下生产一些其他的属性,比如没有指定primaryColor,并且当前主题不是深色主题,那么primaryColor会默认为primarySwatch指定的颜色,还有属性indicatorColor也会受primarySwatch影响。

实例:路由换肤

class ThemeTestRoute extends StatefulWidget {
  @override
  _ThemeTestRouteState createState() => _ThemeTestRouteState();
}

class _ThemeTestRouteState extends State<ThemeTestRoute> {
  var _themeColor = Colors.teal; //当前路由主题色

  @override
  Widget build(BuildContext context) {
    ThemeData themeData = Theme.of(context);
    return Theme(
      data: ThemeData(
          primarySwatch: _themeColor, //用于导航栏、FloatingActionButton的背景色等
          iconTheme: IconThemeData(color: _themeColor) //用于Icon颜色
      ),
      child: Scaffold(
        appBar: AppBar(title: Text("主题测试")),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            //第一行Icon使用主题中的iconTheme
            Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Icon(Icons.favorite),
                  Icon(Icons.airport_shuttle),
                  Text("  颜色跟随主题")
                ]
            ),
            //为第二行Icon自定义颜色(固定为黑色)
            Theme(
              data: themeData.copyWith(
                iconTheme: themeData.iconTheme.copyWith(
                    color: Colors.black
                ),
              ),
              child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    Icon(Icons.favorite),
                    Icon(Icons.airport_shuttle),
                    Text("  颜色固定黑色")
                  ]
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
            onPressed: () =>  //切换主题
                setState(() =>
                _themeColor =
                _themeColor == Colors.teal ? Colors.blue : Colors.teal
                ),
            child: Icon(Icons.palette)
        ),
      ),
    );
  }
}

注意:

  • 可以通过局部主题覆盖全局主题,如实例中Theme可以固定颜色不随主题颜色改变,可以覆盖是因为Widget中使用主题样式时通过Theme.of(BuildContext context)来获取的,简化代码:
static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
   // 简化代码,并非源码  
   return context.dependOnInheritedWidgetOfExactType<_InheritedTheme>().theme.data
}

context.dependOninheritedWidgetOfExactType会在Widget树中从当前位置向上查找第一个类型为_InheritedTheme的Widget。所以当局部指定Theme后,子树中通过Theme.of()向上找到的第一个_InheritedTheme便是我们指定的Theme。

  • 如果想针对整个程序换肤,则可以修改MaterialApp的theme属性。

颜色

Flutter中Color类颜色以int值保存,显示器颜色有红、绿、蓝三基色组成,每种颜色占8比特,结构如下:

Bit位颜色
0-7蓝色
8-15绿色
16-23红色
24-31Alpha(不透明色)

将颜色字符串转换成Color对象

Color(0xffdc380d); //如果颜色固定可以直接使用整数值
//颜色是一个字符串变量
var c = "dc380d";
Color(int.parse(c,radix:16)|0xFF000000) //通过位运算符将Alpha设置为FF
Color(int.parse(c,radix:16)).withAlpha(255)  //通过方法将Alpha设置为FF

颜色亮度

实现一个北京颜色和Title可以自定义的导航栏,并且Title颜色可以根据背景色的深浅变化,要实现这个需求则需要计算背景色的亮度,然后动态的给Title赋值颜色。Color类中提供了一个computeLuminance()函数,它可以返回一个0-1的值,数字越大则颜色越浅,可以根据它的动态确定Title的颜色:

class NavBar extends StatelessWidget {
  final String title;
  final Color color; //背景颜色

  NavBar({
    Key? key,
    required this.color,
    required this.title,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      constraints: BoxConstraints(
        minHeight: 52,
        minWidth: double.infinity,
      ),
      decoration: BoxDecoration(
        color: color,
        boxShadow: [
          //阴影
          BoxShadow(
            color: Colors.black26,
            offset: Offset(0, 3),
            blurRadius: 3,
          ),
        ],
      ),
      child: Text(
        title,
        style: TextStyle(
          fontWeight: FontWeight.bold,
          //根据背景色亮度来确定Title颜色
          color: color.computeLuminance() < 0.5 ? Colors.white : Colors.black,
        ),
      ),
      alignment: Alignment.center,
    );
  }
}

MaterialColor

MaterialColor是实现Material Design中的颜色的类,它包含一种颜色的10个级别的渐变色,,通过[]运算符的索引值来代表颜色的深度,有效索引有:50、100、200-900,数字越大颜色越深。MaterialColor的默认值索引等于500的颜色。

小知识-导航栏返回拦截

为避免用户误触返回按钮导致App退出,大多APP中都拦截了用户点击返回键的按钮,然后进行一些误触判断,比如一段时间内点击两次时,才会认为用户需要退出。Flutter中可以通过willPopScope来实现返回按钮拦截:

const WillPopScope({
  ...
  required WillPopCallback onWillPop,
  required Widget child
})
  • onWillPop是一个回调函数,当用户点击返回时被调用(包括导航栏返回按钮及android物理返回按钮)。该函数需要返回一个Future对象,如果返回的Future最终值为false时,则当前路由不出栈;最终值返回为true时,当前路由出栈退出。

实例:

import 'package:flutter/material.dart';

class WillPopScopeTestRoute extends StatefulWidget {
  @override
  WillPopScopeTestRouteState createState() {
    return WillPopScopeTestRouteState();
  }
  //WillPopScopeTestRouteState createState() => WillPopScopeTestRouteState();
}

class WillPopScopeTestRouteState extends State<WillPopScopeTestRoute> {
  DateTime? _lastPressedAt; //上次点击时间

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        if (_lastPressedAt == null ||
            DateTime.now().difference(_lastPressedAt!) > Duration(seconds: 1)) {
          //两次点击间隔超过1秒则重新计时
          _lastPressedAt = DateTime.now();
          return false;
        }
        return true;
      },
      child: Container(
        alignment: Alignment.center,
        child: Text("1秒内连续按两次返回键退出"),
      ),
    );
  }
}