手记

Flutter——功能型Widget(2)

颜色和主题

颜色

Color类中颜色以一个int值保存,我们知道显示器颜色是由红、绿、蓝三基色组成,每种颜色站8比特,存储结构如下:

bit(位) 颜色
0~7 蓝色
8~15 黄色
16~23 红色
24~31 Alpha (不透明度)

如何将颜色字符串转成Color对象

“#dc380d”,它是一个RGB值,我们可以通过下面这些方法将其转为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显示为深色。要实现这个功能,我们就需要来计算背景色的亮度,然后动态来确定Title的颜色。
Color类中提供了一个computeLuminance()方法,它可以返回一个[0-1]的一个值,数字越大颜色就越浅,我们可以根据它来动态确定Title的颜色,下面是导航栏NavBar的简单实现:

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

  NavBar({
    Key key,
    this.color,
    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,
    );
  }
}

测试代码如下:

Column(
  children: <Widget>[
    //背景为蓝色,则title自动为白色
    NavBar(color: Colors.blue, title: "标题"), 
    //背景为白色,则title自动为黑色
    NavBar(color: Colors.white, title: "标题"),
  ]
)

MaterialColor

MaterialColor是实现Material Design中的颜色的类,它包含一种颜色的10个级别的渐变色。MaterialColor通过"[]"运算符的索引值来代表颜色的深度,有效的索引有:50,100,200,…,900,数字越大,颜色越深。MaterialColor的默认值为索引等于500的颜色。
举个例子,Colors.blue是预定义的一个MaterialColor类对象,定义如下:

static const MaterialColor blue = MaterialColor(
  _bluePrimaryValue,
  <int, Color>{
     50: Color(0xFFE3F2FD),
    100: Color(0xFFBBDEFB),
    200: Color(0xFF90CAF9),
    300: Color(0xFF64B5F6),
    400: Color(0xFF42A5F5),
    500: Color(_bluePrimaryValue),
    600: Color(0xFF1E88E5),
    700: Color(0xFF1976D2),
    800: Color(0xFF1565C0),
    900: Color(0xFF0D47A1),
  },
);
static const int _bluePrimaryValue = 0xFF2196F3;

Theme

Theme组件可以为Material APP定义主题数据(ThemeData)。Material组件库里很多组件都使用了主题数据,如导航栏颜色、标题字体、Icon样式等。Theme内会使用InheritedWidget来为其子树共享样式数据。

ThemeData

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

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

ThemeData({
  Brightness brightness, //深色还是浅色
  MaterialColor primarySwatch, //主题颜色样本,见下面介绍
  Color primaryColor, //主色,决定导航栏颜色
  Color accentColor, //次级色,决定大多数Widget的颜色,如进度条、开关等。
  Color cardColor, //卡片颜色
  Color dividerColor, //分割线颜色
  ButtonThemeData buttonTheme, //按钮主题
  Color cursorColor, //输入框光标颜色
  Color dialogBackgroundColor,//对话框背景颜色
  String fontFamily, //文字字体
  TextTheme textTheme,// 字体主题,包括标题、body等文字样式
  IconThemeData iconTheme, // Icon的默认样式
  TargetPlatform platform, //指定平台,应用特定平台控件风格
  ...
})

上面属性中需要说明的是primarySwatch,它是主题颜色的一个"样本色",通过这个样本色可以在一些条件下生成一些其它的属性,例如,如果没有指定primaryColor,并且当前主题不是深色主题,那么primaryColor就会默认为primarySwatch指定的颜色,还有一些相似的属性如accentColor 、indicatorColor等也会受primarySwatch影响。

示例

路由换肤功能:

import 'package:flutter/material.dart';

class ThemeRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new _ThemeRouteState();
  }

}

class _ThemeRouteState extends State<ThemeRoute> {
  Color _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("Theme Demo"),),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            //第一行Icon使用主题中的iconTheme
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Icon(Icons.favorite),
                Icon(Icons.airline_seat_recline_normal),
                Text("  颜色跟随主题")
              ],
            ),
            //为第二行Icon自定义颜色(固定为黑色)
            Theme(
              data: _themeData.copyWith(
                iconTheme: IconThemeData(color: Colors.black),
              ),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Icon(Icons.favorite),
                  Icon(Icons.airline_seat_recline_normal),
                  Text("  颜色固定黑色")
                ],
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () =>
              setState(() =>
              _themeColor =
              _themeColor == Colors.teal ? Colors.blue : Colors.teal
              ),
          child: Icon(Icons.palette),
        ),
      ),
    );
  }
}

需要注意的有三点:

  • 可以通过局部主题覆盖全局主题,正如代码中通过Theme为第二行图标指定固定颜色(黑色)一样,这是一种常用的技巧,Flutter中会经常使用这种方法来自定义子树主题。
    那么为什么局部主题可以覆盖全局主题?这主要是因为widget中使用主题样式时是通过Theme.of(BuildContext context)来获取的,我们看看其简化后的代码:

    static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
       // 简化代码,并非源码  
       return context.inheritFromWidgetOfExactType(_InheritedTheme).theme.data
    }
    

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

  • 本示例是对单个路由换肤,如果想要对整个应用换肤,则可以去修改MaterialApp的theme属性。

异步UI更新(FutureBuilder、StreamBuilder)

Flutter专门提供了FutureBuilder和StreamBuilder两个组件来快速实现这些功能:

  • 在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中我们显式一个加载框,等获取到数据时再渲染页面;
  • 想展示Stream(比如文件流、互联网数据接收流)的进度。

FutureBuilder

FutureBuilder会依赖一个Future,它会根据所依赖的Future的状态来动态构建自身。FutureBuilder构造函数:

FutureBuilder({
  this.future,
  this.initialData,
  @required this.builder,
})
  • future:FutureBuilder依赖的Future,通常是一个异步耗时任务。

  • initialData:初始数据,用户设置默认数据。

  • builder:Widget构建器;该构建器会在Future执行的不同阶段被多次调用,构建器签名如下:

    Function (BuildContext context, AsyncSnapshot snapshot)
    

    snapshot会包含当前异步任务的状态信息及结果信息 ,比如我们可以通过 snapshot.connectionState获取异步任务的状态信息、通过snapshot.hasError判断异步任务是否有错误等等,完整的定义读者可以查看AsyncSnapshot类定义。

另外,FutureBuilder的builder函数签名和StreamBuilder的builder是相同的。

示例

实现一个路由,当该路由打开时我们从网上获取数据,获取数据时弹一个加载框;获取结束时,如果成功则显示获取到的数据,如果失败则显示错误。由于我们还没有介绍在flutter中如何发起网络请求,所以在这里我们不真正去网络请求数据,而是模拟一下这个过程,隔3秒后返回一个字符串:

Future<String> mockNetworkData() async {
  return Future.delayed(Duration(seconds: 2), () => "我是从互联网上获取的数据");
}

FutureBuilder使用代码如下:

import 'package:flutter/material.dart';

class FutureBuilderRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new _FutureBuilderRouteState();
  }

}
class _FutureBuilderRouteState extends State<FutureBuilderRoute> {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Scaffold(
        appBar: AppBar(title: Text('FutureBuilder'),),
        body: Center(
          child: FutureBuilder<String>(
            future: mockNetworkData(),
            builder: (BuildContext context, AsyncSnapshot snapshot) {
              // 请求已结束
              if (snapshot.connectionState == ConnectionState.done) {
                if (snapshot.hasError) {
                  // 请求失败,显示错误
                  return Text("Error: ${snapshot.error}");
                } else {
                  // 请求成功,显示数据
                  return Text("Contents: ${snapshot.data}");
                }
              } else {
                // 请求未结束,显示loading
                return CircularProgressIndicator();
              }
            },
          ),
        ),
      )
    );
  }
  Future<String> mockNetworkData() async {
    return Future.delayed(Duration(seconds: 2), () => "我是从互联网上获取的数据");
  }
}

StreamBuilder

在Dart中Stream 也是用于接收异步事件数据,和Future 不同的是,它可以接收多个异步操作的结果,它常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。StreamBuilder正是用于配合Stream来展示流上事件(数据)变化的UI组件。

StreamBuilder({
  Key key,
  this.initialData,
  Stream<T> stream,
  @required this.builder,
})

和FutureBuilder的构造函数只有一点不同:前者需要一个future,而后者需要一个stream。

示例

创建一个计时器的示例:每隔1秒,计数加1。这里,我们使用Stream来实现每隔一秒生成一个数字:

Stream<int> counter() {
  return Stream.periodic(Duration(seconds: 1), (i) {
    return i;
  });
}

StreamBuilder使用代码如下:

import 'package:flutter/material.dart';
class StreamBuilderRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new _StreamBuilderRouteState();
  }
}
class _StreamBuilderRouteState extends State<StreamBuilderRoute> {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: StreamBuilder<int>(
          stream: counter(),
          builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
            if (snapshot.hasError)
              return Text('Error: ${snapshot.error}');
            switch (snapshot.connectionState) {
              case ConnectionState.none:
                return Text('没有Stream');
              case ConnectionState.waiting:
                return Text('等待数据...');
              case ConnectionState.active:
                return Text('active: ${snapshot.data}');
              case ConnectionState.done:
                return Text('Stream已关闭');
            }
            return null; // unreachable
          },
        ),
      ),
    );
  }
  Stream<int> counter() {
    return Stream.periodic(Duration(seconds: 1), (i) {
      return i;
    });
  }
}

对话框详解

这个内容较多,不总结了。

0人推荐
随时随地看视频
慕课网APP