Flutter如何爲初始路由添加動畫?頁面中單個元素又如何隨路由動起來?

  今天給你們將兩個關於路由的騷操做,雖說項目裏不太會用到,可是看看漲漲姿式老是好的。
  我想你們應該都知道,Flutter在push/pop路由的時候,都是能夠自定義動畫的,路由動畫在Flutter裏面寫起來很是的靈活,通常來講在push的時候帶上一個自定義的Route就能夠了:git

Navigator.of(context).push(
    PageRouteBuilder(
        pageBuilder: (context, _, __) {
            return ProductDetailPage(
                product: product,
            );
        },
        transitionDuration:
            const Duration(milliseconds: 500),
        transitionsBuilder:
            (_, animation, __, child) {
                return FadeTransition(
                    opacity: animation,
                    child: FadeTransition(
                        opacity:
                            Tween(begin: 0.5, end: 1.0).animate(animation),
                                child: child,
                            ),
                        );
            }),
        );
複製代碼

  transitionDuration定義路由動畫時間,transitionsBuilder中能夠自定義路由動畫,旋轉、位移、縮放等等均可以。api

  那麼,你有沒有想過下面這兩個問題呢?bash

  • push/pop路由的時候能夠帶動畫,但是初始頁面該怎麼給它添加路由動畫呢(Flutter爲了能快速打開App,初始路由是默認不帶任何transition變換的)?
  • 我想在新頁面中作這麼一個效果:打開頁面圖片動畫進入,關閉頁面圖片反向退出,除了本身手動控制,有沒有其餘的方法呢?

  其實,這兩個問題都是屬於路由範疇less

  問題一一樣可使用PageRouteBuilder來解決,只是寫法有些不一樣。通常來講咱們都是直接在MaterialApp中設置home屬性,配置初始頁面,這樣寫明顯是不行的啦,home屬性接受的是一個Widget,而不是一個路由。莫急,很快就教大家一個不同的設置初始頁面的寫法。ide

MaterialApp(
    home: MyHomePage(),
)
複製代碼

  問題二徹底能夠在頁面打開的時候播放一個動畫A,而後監聽頁面關閉,關閉時倒着播放動畫A。可是這麼作明顯不太優雅,咱們換個角度來思考,從路由的角度來講,你的圖片進入/退出動畫不就是隨着路由動畫來進行的嗎?它們徹底可使用同一個controller,那麼你只需定義好動畫,將其和路由controller綁定,何時開始動畫,何時結束,都不須要你來手動控制。函數

如何爲初始路由添加動畫

  一樣也是在MaterialApp中設置,可是不是設置home屬性,而是onGenerateRoute屬性,它和routes屬性很像,也是用來配置路由的:佈局

MaterialApp(
    onGenerateRoute: (settings) {
            if (settings.isInitialRoute) {
              return createInitialRoute();
            }
          },
)

Route<dynamic> createInitialRoute() {
    return PageRouteBuilder(
        transitionDuration: const Duration(seconds: 1),
        pageBuilder: (BuildContext context, _, __) {
          return MyHomePage(title: 'Flutter YMUI');
        },
        transitionsBuilder: (_, animation, __, child) {
          return RotationTransition(
            turns: Tween(begin: 0.0, end: 1.0).animate(animation),
            child: ScaleTransition(
              scale: Tween(begin: 0.0, end: 1.0).animate(animation),
              child: child,
            ),
          );
        });
  }
複製代碼

  onGenerateRoute屬性會告訴咱們一個RouteSettings值,這個值有兩個重要的api:動畫

  • settings.isInitialRoute判斷是不是初始路由;
  • settings.name返回路由名(和routes屬性中的路由名同樣,是一個字符串,用於和push/pop時的name配對)

  所以,咱們根據settings.isInitialRoute判斷是不是初始路由,若是是,那麼就替換成咱們的自定義PageRouteBuilder,這個時候是否是就很熟悉啦,咱們能夠肆意添加路由動畫了。運行一下看下效果:ui

  PS:關於homeroutesonGenerateRouteonUnknownRoute屬性的優先級:this

  Navigator會按照home---->routes---->onGenerateRoute---->onUnknownRoute的順序去尋找路由:

  • home,也就是初始路由,路徑爲:/
  • routes,也就是咱們通常定義路由映射的地方,它的優先級會比onGenerateRoute,若是二者定義的路由又重複,確定是先找routes中的;
  • onGenerateRoute優先級最低,用來處理homeroutes都沒有處理的路由,因此通常返回非空值;
  • onUnknownRoute若是說某個路由上面三個都沒處理,那麼就會由onUnknownRoute來處理這個路由。

頁面元素動畫如何和路由動畫綁定

  咱們先看一下效果,理解一下咱們的需求:

  其實很簡單,就是圖片和文字從頁面底部進入和退出,你徹底監聽頁面的打開和退出,手動執行動畫,只是這樣有點兒麻煩。獲取路由動畫須要用到 ModalRoute這個類中的 ModalRoute.of(context).animation方法。
  咱們先看下相關類的繼承關係: PageRouteBuilder<T> ----> PageRoute<T> ----> ModalRoute<T> ----> TransitionRoute<T> ----> OverlayRoute<T> ----> Route<T>,咱們找源碼能夠發現控制路由動畫的 controller是存在於 TransitionRoute中的,因此它的子類都是能夠獲取到這個屬性的,再仔細看源碼就能夠發現,子類中的 ModalRoute有一個 .of(context)的工廠函數,能夠獲取到 Route實例,那麼綁定就能夠這麼寫了(就是將 controller賦給你自定義的圖片變換動畫的 parent):

Animation<double> controller;
Animation<Offset> imageTranslation;

void _buildAniamtion(){
    controller = ModalRoute.of(context).controller;
    imageTranslation = Tween(
        begin: Offset(0.0, 2.0),
        end: Offset(0.0, 0.0),
      ).animate(
        CurvedAnimation(
          parent: controller,
          curve: Interval(0.0, 0.67, curve: Curves.fastOutSlowIn),
        ),
      );
 }
複製代碼

  其實這麼寫最終運行的時候,效果也是OK的,可是你會發現有一個警告:

info: The member 'controller' can only be used within instance members of subclasses of 'package:flutter/src/widgets/routes.dart'.

  也就是說,Flutter是不建議你在頁面中獲取這個controller的,由於這個屬性有@protected註解,雖然說最後運行效果是沒問題的,可是有個警告老是不太好的。源碼註釋終有一句話,說是controller控制的動畫是經過animation暴露出來的,因此,咱們不妨來看看這個animation。咱們很容易就能找到animationsecondaryAnimation成員變量,是否是看起來很熟悉?建立路由時的pageBuilder就給了咱們兩個動畫值,就是這兩個動畫啦。因此ModalRoute.of(context).animation獲取到的animation就是PageRouteBuilder構建時,pageBuilder屬性中包含的animation

/// animation 對應 ModalRoute.of(context).animation
/// secondaryAnimation 對應 ModalRoute.of(context).secondaryAnimation
 pageBuilder: (BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation) {
            return TestPage();
    },
複製代碼

  因此,咱們能夠這麼定義咱們的動畫,將每個animation都和路由的animation綁定起來就能夠了(AnimationController是繼承自Animation<double>的,因此這裏將controlleranimation賦給你自定義的圖片變換動畫的parent是一個效果,這就是爲何最終運行效果是同樣的了):

Animation<double> navAnimation;
  Animation<Offset> imageTranslation;
  Animation<Offset> textTranslation;
  Animation<double> imageOpacity;
  Animation<double> textOpacity;

  void _buildAniamtion(){
    if (navAnimation == null) {
      navAnimation = ModalRoute.of(context).animation;
      imageTranslation = Tween(
        begin: Offset(0.0, 2.0),
        end: Offset(0.0, 0.0),
      ).animate(
        CurvedAnimation(
          parent: navAnimation,
          curve: Interval(0.0, 0.67, curve: Curves.fastOutSlowIn),
        ),
      );
      imageOpacity = Tween(begin: 0.0, end: 1.0).animate(
        CurvedAnimation(
          parent: navAnimation,
          curve: Interval(0.0, 0.67, curve: Curves.easeIn),
        ),
      );
      textTranslation = Tween(
        begin: Offset(0.0, 1.0),
        end: Offset(0.0, 0.0),
      ).animate(
        CurvedAnimation(
          parent: navAnimation,
          curve: Interval(0.34, 0.84, curve: Curves.ease),
        ),
      );
      textOpacity = Tween(begin: 0.0, end: 1.0).animate(
        CurvedAnimation(
          parent: navAnimation,
          curve: Interval(0.34, 0.84, curve: Curves.linear),
        ),
      );
    }
  }
複製代碼

  接下來就剩最後一個問題了,何時來進行這個綁定操做呢?通常來講,動畫的初始化咱們會選擇在initState()中進行,可是若是咱們將_buildAniamtion()方法放入initState()中執行的話,會報以下的錯誤:

The following assertion was thrown building Builder:

  inheritFromWidgetOfExactType(_ModalScopeStatus) or inheritFromElement() was called before _TestPageState.initState() completed.
  When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget.
  Typically references to to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.

  劃報錯重點:最後一段中:can be placed in the didChangeDependencies method,嗯,寫的很清楚了,咱們在didChangeDependencies()中初始化動畫就能夠了:

@override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _buildAniamtion();
  }
複製代碼

  didChangeDependencies()是緊接着initState()後面執行的,源碼註釋中有一句:

It is safe to call [BuildContext.inheritFromWidgetOfExactType] from this method.

  二者的區別我也說不清楚,反正若是你在initState()中執行某些方法報錯,不妨試試放到didChangeDependencies()中去。
  補充:評論有人說將操做放置到 addPostFrameCallback((timeStamp){ }),也就是第一幀繪製完成以後,聽起來頗有道理對不對??可是跟佈局渲染順序是矛盾的,由於第一幀繪製完成也就意味着build()方法走完了,可是咱們的佈局初始化的時候確定是須要用到動畫value的,好比下面這樣:

FractionalTranslation(
    translation: imageTranslation.value,
        hild: HeaderImage(),
    ),
複製代碼

  而初始化佈局的時候,咱們的自定義的圖片aniamtion尚未初始化和綁定好呢,imageTranslation尚未值,會報錯的。若是非要這麼寫,那麼就須要修改一下佈局了,咱們能夠先給imageTranslation賦一個默認值,而後在addPostFrameCallback((timeStamp){ })監聽中再去修改這個值,而後再刷新狀態:

FractionalTranslation(
    translation:
        imageTranslation == null ? 0.0 : imageTranslation.value,
    child: HeaderImage(),
),

WidgetsBinding.instance.addPostFrameCallback((callback) {
      _buildAniamtion();
      setState(() { });
    });
複製代碼

  TestPage.dart完整代碼以下:

class TestPage extends StatefulWidget {
  @override
  _TestPageState createState() => _TestPageState();
}

class _TestPageState extends State<TestPage> {
  Animation<double> controller;
  Animation<Offset> imageTranslation;
  Animation<Offset> textTranslation;
  Animation<double> imageOpacity;
  Animation<double> textOpacity;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
      _buildAniamtion();  // 此處代碼省略,見上面
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedBuilder(
        animation: controller,
        builder: (BuildContext context, Widget child) {
          return Column(
            children: <Widget>[
              FractionalTranslation(
                translation: imageTranslation.value,
                child: HeaderImage(),
              ),
              Expanded(
                child: FractionalTranslation(
                  translation: textTranslation.value,
                  child: AppText(),
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

class AppText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(left: 12.0, right: 12.0, top: 44.0),
      child: Text(
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras non lorem non justo congue feugiat ut a enim. Ut et sem nec lacus aliquet gravida. Mauris viverra lectus nec vulputate placerat. Nullam sit amet blandit massa, volutpat blandit arcu. Vivamus eu tellus tincidunt, vestibulum neque eu, sagittis neque. Phasellus vitae rutrum magna, eu finibus mi. Suspendisse eget laoreet metus. In mattis dui vitae vestibulum molestie. Curabitur bibendum ut purus in faucibus.",
        style: Theme.of(context).textTheme.body2,
      ),
    );
  }
}

class HeaderImage extends StatelessWidget {
  const HeaderImage({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(4.0),
      child: Image.asset(
        "images/food01.jpeg",
        height: 300.0,
        fit: BoxFit.cover,
      ),
    );
  }
}

複製代碼

  PS:如何判斷當前頁面是不是初始頁面?如何獲取當前頁面路由名?
  ModalRoute.of(context).settings能夠拿到當前路由的基礎配置信息,RouteSettings這個類一開始的時候就提到過啦,能夠取到布爾值isInitialRoute和路由名。

關於ModalRoute的更多屬性,能夠看下這個:Flutter當前路由屬性詳解

相關文章
相關標籤/搜索