Flutter 實現底部擴散模糊動畫(一)跳轉頁面

相關文章

背景

  一直以來,項目組的小夥伴對於某安的設計和交互十分喜好,從首頁佈局到用戶頁樣式到加號擴散動畫,都想用到項目裏來。鑑於他們強烈的熱愛,已經實現了部分佈局看齊。最近,終於輪到了要實現點擊底部加號後出現擴散動畫,並出現幾項操做項的動畫的時候了。git

簡介

  閱讀這篇文章前,你須要對Flutter有必定的瞭解,包括生命週期、高斯模糊、動畫、MediaQuery等相關知識,固然,全部內容均可以經過搜索找到~github

  效果圖:    bash

  交互過程主要分爲如下三步:async

  • 點擊加號,從加號位置以圓形擴散高斯模糊效果;
  • 操做項依次出現,並附帶必定的動畫效果;
  • 點擊"X"或空白處或系統返回鍵,背景以圓形收縮至加號位置。


  完整demo及組件已上傳至項目,走過路過留個star~ide

前置條件

  想要實現效果,首先有幾點前置條件須要明確:佈局

  • 路由須要作成透明路由,不然高斯模糊沒法做用在上一個路由之上;
  • 根據生命週期,動畫的執行必需要在第一次build後當即執行,而不能在initStatedidChangeDependencies裏執行,不然會存在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)

相關文章
相關標籤/搜索