3.1 拟物时钟开发要点
在开发拟物时钟时,需要用到一些Flutter中最为常用的布局组件,如Container容器组件、Stack层叠布局组件。同时也需要对Flutter自定义视图绘制、动画进行学习。因此,本节先介绍这部分内容,完成基础知识学习。之后给出拟物时钟的产品原型,最后进行实战。
3.1.1 使用Container定制组件展示效果
在UI开发中经常需要对视图添加边距、内距等效果,Flutter提供了一个非常方便的Con-tainer组件,能够方便地控制组件边距、大小、位置等属性。不仅如此,通过Container还能实现更多高级展示效果,比如基于Container实现一个带有新拟物风格的时钟表盘,具体实现方法将在实战环节中介绍。
创建一个新的Flutter工程作为Container学习工程,参照2.5节的实例代码替换lib/main.dart。这样在练习时,只需要将Text('Hello!')替换为接下来的Container组件示例即可。
1设置Container背景色
通过Container组件的color属性可以修改背景颜色,例如:
代码运行效果如图3-1所示。
2 设置Container子组件
通过Container的child属性传入子组件:
其中,FlutterLogo是Flutter提供的一个组件,用于展示Flutter的Logo标识。代码运行效果如图3-2所示。
图3-1 Container修改背景为绿色
图3-2 Container指定child组件
3 设置Container对齐方式
Container的尺寸默认会充满父布局,在上面代码中,FlutterLogo的尺寸小于Container,默认会摆放在左上角位置。通过Container的alignment属性可以更改对齐方式,例如,更改为右下角对齐:
代码运行效果如图3-3所示。
除了Alignment.bottomRight外,Flutter提供的默认对齐方式见表3-1。如果默认对齐方式无法满足,也可以通过构造Alignment实例的方式手动指定精确位置。
图3-3 Container指定右下角对齐
表3-1 Flutter Alignment对齐方式
4 设置Container外边距
通过Container的margin属性可以指定外边距,示例如下:
代码运行效果如图3-4所示。
其中,外边距具体分为上下左右4条,EdgeInsets.all的作用是将这4条边距设为同一个值。除此之外,也可以通过EdgeInsets.fromLTRB分别进行设置,如方法名称中的缩写LTRB,设置的顺序分别为左、上、右、下。具体代码如下:
代码运行效果如图3-5所示,可以看出左边距与上边距明显不同。
5 设置Container内边距
通过Container的padding属性进行内边距设置。内边距也是基于EdgeInsets进行设置的。示例代码如下:
图3-4 Container指定外边距效果
图3-5 Container通过fromLTRB指定外边距效果
代码运行效果如图3-6所示。与图3-5对比可以看出,Container内部举例FlutterLogo的内边距明显加大了。
图3-6 Container指定内边距效果
6 通过BoxDecoration添加边界
Container支持装饰器机制,能够为容器添加更高级的样式效果,例如,添加边界、修改形状,以及添加阴影等。装饰器通过Container的decoration属性传入,Flutter提供BoxDecoration类,封装了常用的样式定制功能。
例如,通过下面代码为Container添加一个圆角、带有红色边界的边框:
其中,通过BoxDecoration的border属性指定边框,边框类Border的创建方法与EdgeInsets有些类似,也分为上、下、左、右4条边框,通过Border.all将4种指定为同一个值。边框包含3个属性,分别是颜色、粗细,以及边框样式。borderRadius属性用于指定圆角,传入BorderRadius.circular指定一个圆角值。
需要注意的一点是,上面代码中将color背景色移入BoxDecoration下,而不是像之前代码那样放在Container下。这是因为Container的color属性与decoration属性不能同时指定。当指定decoration后,如果需要设置Container的背景色,由于其已被装饰器接管,因此要设置到装饰器中。
运行代码效果如图3-7所示。
图3-7 Container指定边框效果
7 通过BoxDecoration修改形状
通过BoxDecoration的shape属性可以修改Container的形状。在本章的时钟实战项目中,即通过这个属性实现圆形表盘的绘制。示例代码如下:
代码运行效果如图3-8所示。
8 通过BoxDecoration添加阴影效果
BoxDecoration还支持向Container添加阴影,具体通过boxShadow属性,传入一个BoxShad-ow实例的列表。BoxShadow用于描述阴影,其中color表示阴影颜色,offset表示阴影偏移,blurRadius表示阴影边缘的模糊程度。示例代码如下:
执行代码,效果如图3-9所示。本章后续实战项目中的新拟物设计风格也是通过BoxShad-ow属性实现的。
图3-8 Container指定形状效果
图3-9 Container指定阴影效果
3.1.2 使用CustomPaint创建Flutter自定义视图
像按钮、文本这些常见组件,都是“画”到屏幕上的。这里所说的“画”,与现实中人们绘画的过程是一样的。首先需要有一块画布(Canvas),之后使用画笔(Paint)在上面作画。不同之处是人们绘制的是自然的线条,而计算机绘制的是规整的矩形、直线、文本等。
自定义视图,就是由开发者通过程序来设定组件的绘制流程。为此,Flutter提供了CustomPaint组件,它用于在界面中创建一个指定大小的Canvas,并通过painter属性指定具体的作画流程。
创建一个新的Flutter工程作为CustomPaint学习工程,参照2.5节的实例代码替换lib/main.dart。在后续小节中,只需要用对应的CustomPaint组件替换掉Text组件即可。
1 CustomPaint组件
CustomPaint是Flutter中专门用于绘制自定义视图的组件,提供了对底层绘制API的访问能力。首先举一个简单的例子,在屏幕中绘制一个红色的圆圈,替换MyHomePage的代码如下:
运行显示效果如图3-10所示。
上面代码中的RedCirclePainter部分通过复写的paint方法指定具体的绘制过程。paint方法包含两个参数:Canvas是Flutter实际的画布类,Size为此画布的大小。在绘制过程中,首先创建画笔实例paint,并指定了颜色、填充类型及线条粗细。之后通过canvas.drawCircle方法传入圆心、半径及画笔,在画布上绘制出红色的圆圈。
CustomPaint组件包含的属性见表3-2。
图3-10 绘制红色圆圈
表3-2 CustomPaint组件属性
2 Canvas画布类
在CustomPainter的paint方法中的canvas参数,是用于绘制的画布。可以将画布看作一个二维坐标系,通过传入坐标在其上进行绘制。在Flutter中,坐标系的方向如图3-11所示。
图3-11 Canvas坐标系
除了绘制圆形之外,canvas还提供了一系列绘制方法,用于在画布上绘制不同的图形元素。下面列举常用的绘制方法。
使用drawLine绘制直线,示例代码如下,只需要替换paint方法即可:
运行效果如图3-12所示。
3 Paint画笔类
Paint类用于控制在Canvas上绘制时的绘制样式。大多数Canvas绘制方法需要传入一个Paint对象,并按照Paint对象中的绘制样式进行绘制。
在前面的代码中已经用到了Paint类,通过其color属性更改颜色,通过strokeWidth属性更改线条粗细。
Paint常用属性见表3-3。
图3-12 drawLine直线绘制效果
表3-3 Paint常用属性
3.1.3 Flutter动画入门与拟物时钟的开发流程
良好的用户体验是移动端应用的特色之一,移动端应用通常会加入大量动画元素提升应用的交互体验。Flutter框架在设计时充分考虑了动画的重要性,提供了一个非常强大且易用的动画开发框架,能够快速开发出复杂动画,并高效执行。
所谓动画,实际上是屏幕上的元素以一定的时间间隔进行位置变换。由于时间间隔非常短,且移动的距离非常小,从人眼感受来看,这个过程是流畅平滑的。如果感到难以理解,可以在网络上查阅一下“定格动画”,这是一种历史悠久的动画形式,它的原理是将实物摆放好后进行拍照,之后对实物进行微调后再拍照,如此往复得到了大量照片,然后再以一定的速度播放这些照片,人眼感受到的就是流畅的动画。
1 动画要素
从开发角度来看,动画效果可以被拆解为值的变化。比如一个视图元素横向移动,实际上是这个元素的横坐标在移动,而横坐标的移动,可以被抽象为一个值随着时间变化,这个过程被称为插值。
从插值的角度,可将动画过程拆分为以下要素。
(1)起始值和终止值
以平移动画为例,起始值为元素的起始横坐标,终止值为元素在动画终止时刻的横坐标。
对于一个动画过程,开发者首先要指定起始值和终止值。对于变色动画来说,起始值为元素的起始颜色,终止值为动画终止时的目标颜色。
(2)插值
在动画过程中,从起始值到终止值的变化过程由插值器产生。插值器的作用是随着时间变化生成动画的中间状态,这一个过程称之为插值。
以平移动画为例,假设元素匀速运动,整个动画过程为1s,在指定好起始值和终止值后,启动动画,插值器会根据当前时间计算此时元素应当处于哪个位置,并以回调的方式将这个值返回。开发者通常在回调中接收这个值,并通过setState方法更新元素的横坐标。
插值还分为匀速插值与变速插值。匀速插值比较好理解,从起始值以恒定的步长运动到终止值。而变速插值的步长是不固定的,从而形成变速运动效果。以变速平移为例,通过使用变速插值,可以让元素运动“先快后慢”“先慢后快”,或者“先快,中间慢,最后再快”。
变速插值在移动应用中被广泛使用,常见的弹层、悬浮框等都大量采用变速插值进行展示、隐藏,能够给用户带来更加自然、流畅的使用体验。在本章的实战项目拟物时钟中,表针的转动就应用了变速插值,能让用户感受到表针像真实表针一样在转动,增强了拟物感。
(3)动画时长
动画时长指定整个动画的执行时间,决定了动画执行的快与慢。以平移动画为例,假设动画时长设置非常长,在同样的平移距离下,物体移动会非常缓慢。
(4)帧
屏幕是以帧进行渲染的,手机会以每秒60帧或更高的刷新率对屏幕进行刷新,称为帧率。看似连续的动画过程,实际是由插值器拆成离散的值后,通过Flutter的渲染机制渲染为离散的帧后展示出来的。只不过在60帧的帧率下人眼已经无法感受到离散帧的跳变了。目前市场上出现了高刷新率手机,能够达到每秒90帧、每秒120帧甚至更高的刷新率,能够给用户带来更加流畅的使用体验。
2 AnimationController动画控制器
有了前面的知识铺垫后,接下来学习Flutter的动画框架。在Flutter中,针对不同的职责创建了不同的类,比如AnimationController用于动画控制,Tween用于指定起始值和终止值,Curve用于变速插值。通过将这些类结合在一起,共同组装出一个动画Animation。在初学时可能会因为涉及的类和概念比较多而感到有些乱,实际上只要通过学习完成几个示例,就能够体会到这种架构划分的合理性与强大之处。
AnimationController用于控制动画过程,职责是控制动画的启停。通过以下代码可以创建一个AnimationController:
在上面的代码中,传入了两个参数,duration控制动画时长,vsync是一个特殊参数,用于优化动画资源,实际传入类型为TickerProvider。TickerProvider会在后文中进行讲解,这里先介绍动画框架的使用。
AnimationController创建完成后,通过forward可以启动动画:
在页面退出时,如果有动画还在执行,会导致内存泄漏。AnimationController提供了一个dispose方法,用于销毁动画。AnimationController的典型使用方法为在类中作为一个属性,在initState中进行创建,在页面dispose时进行释放,如下面代码所示:
在上述代码中,关于SingleTickerProviderStateMixin可暂时忽略,将在后文中进行讲解。
3 执行动画并打印插值过程
前面提到了插值的概念,AnimationController中自带有一个匀速插值器,并且默认起始值为0,终止值为1。下面以一个实例介绍如何执行动画。创建一个组件,代码如下:
运行代码,可以看到命令行中输出了插值,注意观察日志时间的变化:
4 使用Tween设置起始值与结束值
AnimationController默认起始值为0,结束值为1,对于平移动画来说,希望能够自定义区间值。这时可使用Tween组件对起止值进行更改。在Tween的animate方法中,传入一个Ani-mationController实例。对initState方法进行修改,示例代码如下:
在上面的代码中,修改了插值监听回调的添加位置,之前是在AnimationController上添加监听,现在是在Tween.animate返回的animation对象上监听,同时取插值也改成了从animation对象中获取。运行代码,命令行输出日志如下:
5 完成平移动画
到目前为止都是在打印插值数值,build方法的布局中只有一个静态的Container方法。下面创建一个矩形,并使其平移运动起来。修改组件如下:
再次运行代码,可看到红色矩形的平移运动,具体如图3-13所示。
图3-13 视图平移动画
6 CurvedAnimation变速插值
前面介绍的AnimationController功能为匀速插值,如果要实现变速插值,需要使用CurvedAnimation。
CurvedAnimation的功能建立在AnimationController之上,在创建变速动画时,首先要创建一个AnimationCon-troller实例,之后再创建CurvedAnimation实例,并将Ani-mationController传入CurvedAnimation的构造方法。示例代码如下:
在上面的代码中,CurvedAnimation还接收一个curve参数,用于传入变速插值曲线。在Flutter中预设了数十种变速曲线,可访问https://api.flutter.dev/flutter/animation/Curves-class.html进行查看。以Curves.elasticInOut为例,其变速曲线如图3-14所示。
图3-14 Curves.elasticInOut变速曲线
在平移动画示例中,修改initState方法,实现变速动画,具体代码如下:
再次运行程序,可以看到红色方块由匀速运动变为变速运动,并且按照Curves.elasticInOut的变速曲线实现了一种类似于蓄力冲刺的运动效果,使得原本平淡无奇的动画变得富有动感。
7 TickerProvider
之所以将TickerProvider放到后面来讲,是因为它与动画的创建与使用关系不大,而是与动画的底层渲染相关。通过前面的小节完成了对Flutter动画的入门后,本节重点介绍TickerPro-vider。
为了更好地理解Flutter的TickerProvider机制,先设想如果由开发者自己实现一个动画框架,会是什么样子呢?首先需要创建一个定时器,每次触发时根据触发时间计算插值器的值,并对上层进行回调。问题在于定时器的时间间隔,如果间隔太长,比如500 ms刷新一次,假设一个动画总共时长才1s,即它在动画过程中只插值了一个中间点,从用户角度看相当不流畅。如果间隔设置太短,比如0.1 ms刷新一次,这样能够在动画过程中插入足够的中间点,保证动画的流畅性,但是屏幕的刷新率是固定的(60 Hz、120 Hz),如果动画的刷新率高于屏幕刷新率,多出来的中间点用户是看不到的,白白增加了性能损耗。
最好的方式是什么样的呢?是动画的间隔跟屏幕的刷新率保持一致,保证每次动画中间状态的更新都被渲染在屏幕上,这就是TickerProvider的作用。在Flutter动画框架中,包含以下概念。
• Ticker:会在每帧开始时触发回调。
• TickerProvider:通常实现为SingleTickerProviderStateMixin,是对Ticker进行封装。
通过向AnimationController中传入TickerProvider,就能监听屏幕刷新,保证动画的刷新率与屏幕刷新率一致。这样,既避免了动画刷新慢导致的不流畅,也避免了刷新过快导致的资源浪费。
8 使用AnimatedBuilder高效开发动画
前面介绍了创建动画的方法,即使用AnimationController,结合Tween、CurvedAnimation创建动画效果。这是一种通用的动画创建机制,通过组合能够创建出复杂的动画效果。
但是在实际中,AnimationController需要在类中通过属性持有,需要在initState和dispose中进行初始化与销毁,使用起来比较烦琐。有没有一种更加简单、高效的动画开发方式呢?答案是肯定的,Flutter提供了AnimatedBuilder用于以组件化的形式创建动画。
比较常用的是TweenAnimationBuilder,它通过几个属性对动画进行配置,tween属性用于设置动画的起止值,curve属性用于设置变速曲线,duration属性设置动画时长,builder属性接收一个回调方法,在方法参数中含有插值,在函数中更新组件的最新状态并返回。使用Tween-AnimationBuilder实现弹性平移动画的代码如下:
在上面的代码中,可以看出通过使用TweenAnimationBuilder,代码量有所降低,同时也实现了统一的组件化编程风格,提高了开发效率和代码质量。
有了理论开发知识作为铺垫,下面进行拟物时钟的产品功能构思。拟物时钟是一款仿照现实的时钟,同时带有拟物化设计风格。移动端设计风格经历过从拟物化到扁平化的转变,近年来在扁平化的基础上诞生了一种新拟物化设计风格,非常美观。在拟物时钟项目中,使用这种新拟物设计风格。
拟物时钟的原型图如图3-15所示。其中包含时针与分针用于指示时间,还包括一个文本框,用于展示当前的日期。原型图仅用于示意产品的功能,因此看起来有些“简陋”,通过在接下来实战中添加拟物设计样式,会让最终的效果变得“潮”起来。
图3-15 拟物时钟原型图