動畫與打包
動畫
? Flutter中的動畫系統(tǒng)基于Animation
對象的,和之前的手勢不同,它不是一個Widget,這是因為Animation
對象本身和UI渲染沒有任何關(guān)系。Animation是一個抽象類,就相當于一個定時器,它用于保存動畫的插值和狀態(tài),并執(zhí)行數(shù)值的變化。widget可以在build
函數(shù)中讀取Animation
對象的當前值, 并且可以監(jiān)聽動畫的狀態(tài)改變。
AnimationController
? AnimationController用于控制動畫,它包含動畫的啟動forward()
、停止stop()
、反向播放 reverse()
等方法。AnimationController會在動畫的每一幀,就會生成一個新的值。默認情況下,AnimationController在給定的時間段內(nèi)線性的生成從0.0到1.0(默認區(qū)間)的數(shù)字。
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000), //動畫時間
lowerBound: 10.0, //生成數(shù)字的區(qū)間
upperBound: 20.0, //10.0 - 20.0
vsync: this //TickerProvider 動畫驅(qū)動器提供者
);
Ticker
? Ticker的作用是添加屏幕刷新回調(diào),每次屏幕刷新都會調(diào)用TickerCallback
。使用Ticker來驅(qū)動動畫會防止屏幕外動畫(動畫的UI不在當前屏幕時,如鎖屏時)消耗不必要的資源。因為Flutter中屏幕刷新時會通知Ticker,鎖屏后屏幕會停止刷新,所以Ticker就不會再觸發(fā)。最簡單的做法為將SingleTickerProviderStateMixin
添加到State的定義中。
import 'package:flutter/material.dart';
void main() => runApp(AnimationApp());
class AnimationApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "animation",
home: Scaffold(
appBar: AppBar(
title: Text('animation'),
),
body: AnimWidget(),
),
);
}
}
// 動畫是有狀態(tài)的
class AnimWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AnimWidgetState();
}
}
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
bool forward = true;
@override
void initState() {
super.initState();
controller = AnimationController(
// 動畫的時長
duration: Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 100.0,
// 提供 vsync 最簡單的方式,就是直接混入 SingleTickerProviderStateMixin
// 如果有多個AnimationController,則使用TickerProviderStateMixin。
vsync: this,
);
//狀態(tài)修改監(jiān)聽
controller
..addStatusListener((AnimationStatus status) {
debugPrint("狀態(tài):$status");
})
..addListener(() {
setState(() => {});
});
debugPrint("controller.value:${controller.value}");
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
width: controller.value,
height: controller.value,
color: Colors.blue,
),
RaisedButton(
child: Text("播放"),
onPressed: () {
if (forward) {
controller.forward();
} else {
controller.reverse();
}
forward = !forward;
},
),
RaisedButton(
child: Text("停止"),
onPressed: () {
controller.stop();
},
)
],
);
}
}
動畫狀態(tài)監(jiān)聽:在forword結(jié)束之后狀態(tài)為completed。在reverse結(jié)束之后狀態(tài)為dismissed
Tween
? 默認情況下,AnimationController
對象值為:double類型,范圍是0.0到1.0 。如果我們需要不同的范圍或不同的數(shù)據(jù)類型,則可以使用Tween來配置動畫以生成不同的范圍或數(shù)據(jù)類型的值。要使用Tween對象,需要調(diào)用其animate()
方法,然后傳入一個控制器對象,同時動畫過程中產(chǎn)生的數(shù)值由Tween
的lerp
方法決定。
import 'package:flutter/material.dart';
void main() => runApp(AnimationApp());
class AnimationApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "animation",
home: Scaffold(
appBar: AppBar(
title: Text('animation'),
),
body: AnimWidget(),
),
);
}
}
// 動畫是有狀態(tài)的
class AnimWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AnimWidgetState();
}
}
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
bool forward = true;
Tween<Color> tween;
@override
void initState() {
super.initState();
controller = AnimationController(
// 動畫的時長
duration: Duration(milliseconds: 2000),
// 提供 vsync 最簡單的方式,就是直接繼承 SingleTickerProviderStateMixin
vsync: this,
);
//使用Color
tween = ColorTween(begin: Colors.blue, end: Colors.yellow);
//添加動畫值修改監(jiān)聽
tween.animate(controller)..addListener(() => setState(() {}));
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
width: 100,
height: 100,
//獲取動畫當前值
color: tween.evaluate(controller),
),
RaisedButton(
child: Text("播放"),
onPressed: () {
if (forward) {
controller.forward();
} else {
controller.reverse();
}
forward = !forward;
},
),
RaisedButton(
child: Text("停止"),
onPressed: () {
controller.stop();
},
)
],
);
}
}
Curve
? 動畫過程默認是線性的(勻速),如果需要非線形的,比如:加速的或者先加速后減速等。Flutter中可以通過Curve(曲線)來描述動畫過程。
import 'package:flutter/material.dart';
void main() => runApp(AnimationApp());
class AnimationApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "animation",
home: Scaffold(
appBar: AppBar(
title: Text('animation'),
),
body: AnimWidget(),
),
);
}
}
// 動畫是有狀態(tài)的
class AnimWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AnimWidgetState();
}
}
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
bool forward = true;
@override
void initState() {
super.initState();
controller = AnimationController(
// 動畫的時長
duration: Duration(milliseconds: 2000),
// 提供 vsync 最簡單的方式,就是直接繼承 SingleTickerProviderStateMixin
vsync: this,
);
//彈性
animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
//使用Color
animation = Tween(begin: 10.0, end: 100.0).animate(animation)
..addListener(() {
setState(() => {});
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
//不需要轉(zhuǎn)換
width: animation.value,
height: animation.value,
//獲取動畫當前值
color: Colors.blue,
),
RaisedButton(
child: Text("播放"),
onPressed: () {
if (forward) {
controller.forward();
} else {
controller.reverse();
}
forward = !forward;
},
),
RaisedButton(
child: Text("停止"),
onPressed: () {
controller.stop();
},
)
],
);
}
}
AnimatedWidget
? 通過上面的學習我們能夠感受到Animation
對象本身和UI渲染沒有任何關(guān)系。而通過addListener()
和setState()
來更新UI這一步其實是通用的,如果每個動畫中都加這么一句是比較繁瑣的。AnimatedWidget類封裝了調(diào)用setState()
的細節(jié),簡單來說就是自動調(diào)用setState()
。
? Flutter中已經(jīng)封裝了很多動畫,比如對widget進行縮放,可以直接使用ScaleTransition
import 'package:flutter/material.dart';
void main() => runApp(AnimationApp());
class AnimationApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "animation",
home: Scaffold(
appBar: AppBar(
title: Text('animation'),
),
body: AnimWidget(),
),
);
}
}
// 動畫是有狀態(tài)的
class AnimWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AnimWidgetState();
}
}
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
bool forward = true;
@override
void initState() {
super.initState();
controller = AnimationController(
// 動畫的時長
duration: Duration(milliseconds: 2000),
// 提供 vsync 最簡單的方式,就是直接繼承 SingleTickerProviderStateMixin
vsync: this,
);
//彈性
animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
//使用Color
animation = Tween(begin: 10.0, end: 100.0).animate(animation);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ScaleTransition(
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
scale: controller,
),
RaisedButton(
child: Text("播放"),
onPressed: () {
if (forward) {
controller.forward();
} else {
controller.reverse();
}
forward = !forward;
},
),
RaisedButton(
child: Text("停止"),
onPressed: () {
controller.stop();
},
)
],
);
}
}
Hero動畫
? Hero動畫就是在路由切換時,有一個共享的Widget可以在新舊路由間切換,由于共享的Widget在新舊路由頁面上的位置、外觀可能有所差異,所以在路由切換時會逐漸過渡,這樣就會產(chǎn)生一個Hero動畫。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主頁"),
),
body: Route1()),
);
}
}
// 路由A
class Route1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topCenter,
child: InkWell(
child: Hero(
tag: "avatar", //唯一標記,前后兩個路由頁Hero的tag必須相同
child: CircleAvatar(
backgroundImage: AssetImage(
"assets/banner.jpeg",
),
),
),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return Route2();
}));
},
),
);
}
}
class Route2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Hero(
tag: "avatar", //唯一標記,前后兩個路由頁Hero的tag必須相同
child: Image.asset("assets/banner.jpeg")),
);
}
}
組合動畫
有些時候我們可能會需要執(zhí)行一個動畫序列執(zhí)行一些復雜的動畫。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Route(),
);
}
}
class Route extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return RouteState();
}
}
class RouteState extends State<Route> with SingleTickerProviderStateMixin {
Animation<Color> color;
Animation<double> width;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
// 動畫的時長
duration: Duration(milliseconds: 2000),
// 提供 vsync 最簡單的方式,就是直接繼承 SingleTickerProviderStateMixin
vsync: this,
);
//高度動畫
width = Tween<double>(
begin: 100.0,
end: 300.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
//間隔,前60%的動畫時間 1200ms執(zhí)行高度變化
0.0, 0.6,
),
),
);
color = ColorTween(
begin: Colors.green,
end: Colors.red,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.6, 1.0, //高度變化完成后 800ms 執(zhí)行顏色編碼
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主頁"),
),
body: InkWell(
///1、不用顯式的去添加幀監(jiān)聽器,再調(diào)用setState()
///2、縮小動畫構(gòu)建的范圍,如果沒有builder,setState()將會在父widget上下文調(diào)用,導致父widget的build方法重新調(diào)用,現(xiàn)在只會導致動畫widget的build重新調(diào)用
child: AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Container(
color: color.value,
width: width.value,
height: 100.0,
);
}),
onTap: () {
controller.forward().whenCompleteOrCancel(() => controller.reverse());
},
),
);
}
}
打包
? Flutter在打Release包時候回使用AOT,因此在對一個Flutter測試時候務必使用Release來進行測試。打包命令:flutter build apk
。當然我們需要打包時,還需要配置一些比如簽名的內(nèi)容。配置這些內(nèi)容和普通Android工程沒有區(qū)別,都是在build.gradle
中進行,只是Flutter工程AS沒有提供GUI。
? 在Flutter工程的android/app
下面的build.gradle
可以修改包名、版本等信息,這就不用多說了。獲得簽名文件之后,將它復制到flutter的android目錄:
? 然后在app的build.gradle
中配置:
signingConfigs {
release {
keyAlias 'enjoy'
keyPassword '123456'
// 因為是放到父級的根目錄,使用rootProject
// 如果放在這個build.gradle的同級,直接使用file
storeFile rootProject.file('enjoy.jks')
storePassword '123456'
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.release
}
}
餅圖
https://github.com/google/charts
Stack布局中的fit屬性與Image的fit類似,表示內(nèi)容的擴充情況。默認為
StackFit.loose
表示Stack
與內(nèi)容一樣大。如果設(shè)置為StackFit.passthrough
則表示Stack
父Widget的約束會傳給Stack內(nèi)部非Positioned的子Widget。效果如代碼中的StackFit.dart