一直以來,項目組的小夥伴對於某安的設計和交互十分喜好,從首頁佈局到用戶頁樣式到加號擴散動畫,都想用到項目裏來。鑑於他們強烈的熱愛,已經實現了部分佈局看齊。最近,終於輪到了要實現點擊底部加號後出現擴散動畫,並出現幾項操做項的動畫的時候了。git
閱讀這篇文章前,你須要對Flutter有必定的瞭解,包括生命週期、高斯模糊、動畫、MediaQuery
等相關知識,固然,全部內容均可以經過搜索找到~github
效果圖: bash
交互過程主要分爲如下三步:async
完整demo及組件已上傳至項目,走過路過留個star~ide
想要實現效果,首先有幾點前置條件須要明確:佈局
build
後當即執行,而不能在initState
或didChangeDependencies
裏執行,不然會存在context
爲空或觸發時機錯誤的問題;pop()
前執行,不然widget
已經被取消掛載(this.mounted == false)
。下面是具體的實現過程,將配合上述條件進行說明。post
網上有很是多的透明路由實例,包括法法路由裏也包含了透明路由,此處再也不贅述,直接貼上代碼。動畫
class TransparentRoute extends PageRoute<void> {
TransparentRoute({
@required this.builder,
RouteSettings settings,
}) : assert(builder != null),
super(settings: settings, fullscreenDialog: false);
final WidgetBuilder builder;
@override
bool get opaque => false;
@override
Color get barrierColor => null;
@override
String get barrierLabel => null;
@override
bool get maintainState => true;
@override
/// 這裏時長設置爲0,是由於咱們的佈局一開始
/// 並不包含任何內容,因此直接砍掉跳轉時間。
Duration get transitionDuration => Duration.zero;
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final result = builder(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: result,
);
}
}
複製代碼
構建完成後,直接push
就OK。ui
Navigator.of(context).push(TransparentRoute(
builder: (context) => AddingButtonPage(),
));
複製代碼
在widget
中實現運行動畫,首先須要加入TickerProviderStateMixin
,而且聲明一個controller
和動畫(Animation
)自己。this
class _DemoPageState extends State<DemoPage>
with TickerProviderStateMixin {
/.../
Animation<double> _backDropFilterAnimation;
AnimationController _backDropFilterController;
複製代碼
在隨後的功能中,咱們首先對controller
進行初始化,設定一個動畫時長。
_backDropFilterController = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
複製代碼
這時咱們開始思考擴散大小的問題:以底部爲中心,半徑逐漸放大的圓,當半徑達到多少時能徹底覆蓋可視範圍呢?
答案:
√ (width² + (height * 2 + padding.top)²) / 2
根號(二倍高的平方加寬的平方)的一半
是否是一個很是熟悉的公式?沒錯,它就是「勾股定理」~
dart:math
簡單實現的勾股定理:
import 'dart:math' as math;
double pythagoreanTheorem(double short, double long) {
return math.sqrt(math.pow(short, 2) + math.pow(long, 2));
}
複製代碼
這裏利用一張圖片說明半徑的問題。
padding.top
是狀態欄的高度,也要加入到高度中。
因此,咱們就肯定了圓形的終止半徑,且起始半徑爲0。這個時候能夠寫出第一個Tween
了,用於肯定圓形半徑的變化範圍。MediaQuery
用於獲取視圖長短邊。順便定義一個曲線,實現曲線過渡效果。Flutter的Curves
裏內置了許多曲線,在這我選用了Curves.easeInOut
。
/// 視野區域的大小(Size)
final MediaQueryData m = MediaQuery.of(context);
final Size s = m.size;
final double r = pythagoreanTheorem(s.width, s.height * 2 + m.padding.top) / 2;
/// 動畫曲線
Animation _backDropFilterCurve = CurvedAnimation(
parent: _backDropFilterController,
curve: Curves.easeInOut,
);
/// 放大動畫的設定檔
Animation<double> _backDropFilterAnimation = Tween(
begin: 0.0, end: r * 2
).animate(_backDropFilterCurve);
複製代碼
此處終止值是兩倍半徑的緣由是圓形的繪製是以圓形的外正方形大小來進行的繪製的,因此此處大小須要設置爲兩倍半徑,以達到真正的半徑效果。
一個動畫的設定檔完成了,要想讓動畫動起來,須要把動畫執行的值和一個變量綁定,而且執行動畫。因此咱們給這個動畫加上監聽後執行setState
以更新大小,而且執行動畫。
/// 保存半徑的變量
double _backdropFilterSize = 0.0;
/// 監聽動畫執行
_backDropFilterAnimation.addListener(() {
setState(() {
_backdropFilterSize = _backDropFilterAnimation.value;
});
});
/// 正向執行動畫
_backDropFilterController.forward();
複製代碼
至此,放大動畫已經完成了設定,接下來咱們建立佈局與該動畫進行綁定。
剛剛在設定動畫時咱們已經知道,圓形的最終大小是遠遠超過視圖可視大小的,在Flutter中想要實現這樣的相對佈局或絕對佈局,咱們須要用到Stack
。這時須要注意,Stack
的溢出屬性(overflow
)須要設置爲顯示,不然圓形只能擴大到視圖最大寬度。
Stack(
overflow: Overflow.visible,
children: <Widget>[],
);
複製代碼
咱們開始來考慮高斯模糊的區域大小。已知圓形的半徑爲對角線長度,那麼以此設定的區域應該是多大呢?
再次拿出一張圖來看看咱們的擴散圓形相對於視圖應該處於什麼位置:
Positioned
使用的是絕對佈局,在此處,它的參考系是視圖區域。那麼咱們能夠很輕易的判斷頂部和橫向的溢出,用於計算大小。
final MediaQueryData m = MediaQuery.of(context);
final Size s = m.size;
final double r = pythagoreanTheorem(s.width, s.height * 2 + m.padding.top) / 2;
/// 頂部溢出大小
final double topOverflow = r - s.height;
/// 橫向溢出大小
final double horizontalOverflow = r - s.width;
return Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
left: - horizontalOverflow,
right: - horizontalOverflow,
top: - topOverflow,
bottom: - r,
/.../
複製代碼
以此設定範圍,就是圓形擴大到最大半徑時外正方形的大小。
在Flutter中實現高斯模糊很是簡單,只須要使用BackdropFilter
便可,一般來講須要在外包裹ClipRect
用來解決模糊區域的問題,而咱們的需求是圓形,因此在這裏應該使用ClipRRect
。
import 'dart:ui' as ui;
Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
left: - horizontalOverflow,
right: - horizontalOverflow,
top: - topOverflow,
bottom: - r,
child: SizedBox(
/// 高寬與變量綁定
width: _backdropFilterSize,
height: _backdropFilterSize,
/// 使用圓角ClipRRect達到圓形效果
child: ClipRRect(
/// 圓角的大小,使用最大值則全部時候都爲圓形
borderRadius: BorderRadius.circular(r * 2),
child: BackdropFilter(
/// XY用於設定模糊程度
filter: ui.ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0),
/// 使用空格佔位,不然模糊背景不顯示
child: Text(" "),
),
),
),
),
],
);
複製代碼
將高斯模糊控件放入佈局中,咱們便完成了圓形的定位。
實現了背景模糊,接下來就是將內容放置在佈局中合理的大小區域。
咱們的圓形上半部分位於可視區域,因此咱們在背景中,使用Align
,利用溢出大小和已知的可視區域大小,即可以肯定內容放置的位置。
Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(...),
Align(
/// 區域相對頂部居中對齊,在可視區域附近
alignment: Alignment.topCenter,
child: Container(
/// 推出頂部溢出部分,使得區域頂部對齊視圖頂部
margin: EdgeInsets.only(top: topOverflow),
/// 將可視區域大小設定爲控件大小
width: s.width,
height: s.height,
/// 設置constraint,防止子控件發生意料以外的溢出
constraints: BoxConstraints(
maxWidth: s.width,
maxHeight: s.height,
),
child: child ?? SizedBox(),
),
);
],
);
複製代碼
至此,咱們能夠很方便地在模糊區域內放置內容了,不須要使用時再去設置佈局。
動畫部分完成,咱們將動畫部分封裝起來,加入到首次完成build
後執行。
import 'package:flutter/scheduler.dart';
class _AddingButtonPageState extends State<AddingButtonPage> with TickerProviderStateMixin {
@override
void initState() {
/// 使用scheduler,將動畫加入到build後進行
SchedulerBinding.instance.addPostFrameCallback((_) => backDropFilterAnimate(context));
super.initState();
}
void backDropFilterAnimate(BuildContext context) async {
final Size s = MediaQuery.of(context).size;
_backDropFilterController = AnimationController(
duration: Duration(milliseconds: _animateDuration),
vsync: this,
);
Animation _backDropFilterCurve = CurvedAnimation(
parent: _backDropFilterController,
curve: Curves.easeInOut,
);
_backDropFilterAnimation = Tween(
begin: 0.0,
end: pythagoreanTheorem(s.width, s.height) * 2,
).animate(_backDropFilterCurve)
..addListener(() {
setState(() {
_backdropFilterSize = _backDropFilterAnimation.value;
});
});
_backDropFilterController.forward();
}
/.../
複製代碼
至此,一個底部擴散模糊動畫跳轉頁面的動畫就這樣輕鬆如意的完成啦~
根據幾個月的潛水經驗,大多數人以爲Flutter製做動畫困難是由於看不懂Animation
的各類屬性和操做,甚至文檔都生澀難懂,可其實真正寫出來後,動畫部分也只有少許代碼,很容易就能夠理解其中的含義。
最後歡迎加入Flutter Candies,一塊兒生產可愛的Flutter小糖果 (QQ羣:181398081)