Flutter Hero動畫案例

Hero 指的是能夠在路由(頁面)之間「飛行」的 widget,簡單來講 Hero 動畫就是在路由切換時,有一個共享的 widget 能夠在新舊路由間切換。因爲共享的 widget 在新舊路由頁面上的位置、外觀可能有所差別,因此在路由切換時會從舊路逐漸過渡到新路由中的指定位置,這樣就會產生一個 Hero 動畫。canvas

實現一個簡單的 Hero 動畫

    
  
  
  
   
   
            
   
   
  1. 微信

  2. app

  3. less

  4. ide

  5. 函數

  6. 學習

  7. flex

  8. 動畫

  9. ui

Container( alignment: Alignment.center, child: InkWell( child: Hero( tag: "tag",//惟一標記,先後兩個路由頁Hero的tag必須相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 80, height: 80, fit: BoxFit.cover, ), )), onTap: () { Navigator.of(context).push( new MaterialPageRoute(builder: (BuildContext context) { return new HeroAnimationPage1(); })); }, ), )
  • HeroAnimationPage1

    
  
  
  
   
   
            
   
   
class HeroAnimationPage1 extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Hero1"), ), body: Center( child: Container( margin: EdgeInsets.all(20), child: Hero( tag: "tag", //惟一標記,先後兩個路由頁Hero的tag必須相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 120, height: 120, fit: BoxFit.cover, ), ), ), ), )); }}

效果圖

能夠看到,實現 Hero 動畫只須要用 Hero 組件將要共享的 widget 包裝起來,並提供一個相同的 tag 便可,中間的過渡幀都是 Flutter Framework 自動完成的。必需要注意, 先後路由頁的共享 Hero 的 tag 必須是相同的,Flutter Framework 內部正是經過 tag 來肯定新舊路由頁 widget 的對應關係的。

但咱們用的是 MaterialPageRoute 這個系統給咱們提供好的路由,這個路由能讓咱們在 Android 或者 Ios 上呈現相應的頁面跳轉效果,但在這裏和 Hero 合起來看有點雜亂,彆扭,咱們稍微改一下:

    
  
  
  
   
   
            
   
   

Container( alignment: Alignment.center, child: InkWell( child: Hero( tag: "tag", //惟一標記,先後兩個路由頁Hero的tag必須相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 80, height: 80, fit: BoxFit.cover, ), )), onTap: () {// Navigator.of(context).push(// new MaterialPageRoute(builder: (BuildContext context) {// return new HeroAnimationPage1();// })); Navigator.push(context, PageRouteBuilder(pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return new FadeTransition( opacity: animation, child: Scaffold( appBar: AppBar( title: Text("Fade"), ), body: HeroAnimationRouteWithFade(), ), ); })); }, ), )
  • HeroAnimationRouteWithFade

    
  
  
  
   
   
            
   
   
class HeroAnimationRouteWithFade extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Container( margin: EdgeInsets.all(20), child: Hero( tag: "tag", //惟一標記,先後兩個路由頁Hero的tag必須相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 120, height: 120, fit: BoxFit.cover, ), ), ), ), ); }}

效果圖 


經過 PageRouteBuilder 自定義本身的路由器,動畫看起來明顯乾淨了不少。

不過細心的我發現,在運動(飛翔)過程當中,圖片在慢慢變大的同時好像也稍微有點變形。這其實我也不知道爲啥,後來我從 Flutter 中文官網看到 MaterialRectCenterArcTween 這個類。這個類竟然能讓控件在運動過程當中圓角不變形。

咱們先回頭看看 Hero 的構造函數:

    
  
  
  
   
   
            
   
   
const Hero({ Key key, @required this.tag, this.createRectTween, this.flightShuttleBuilder, this.placeholderBuilder, this.transitionOnUserGestures = false, @required this.child, }) : assert(tag != null), assert(transitionOnUserGestures != null), assert(child != null), super(key: key);
  • tag:[必須]用於關聯兩個 Hero 動畫的標識,先後兩個路由頁 Hero 的 tag 必須相同.

  • createRectTween:[可選]定義目標 Hero 的邊界,在從起始位置到目的位置的運動過程當中該如何變化。

  • child:[必須]定義動畫所呈現的 widget。

MaterialRectCenterArcTween 這個類正好能給咱們返回 createRectTween 參數所需的實例。

咱們再來看一個稍微高大上的 Hero 動畫,其實我以爲沒啥改動,只是界面作的稍微複雜一點,添加了 createRectTween。

實現一個複雜的 Hero 動畫 

  • 這裏提一下 timeDilation = 3; 這是讓過渡動畫時間稍微慢一點,默認爲 1.

    
  
  
  
   
   
            
   
   
timeDilation = 3; //動畫過渡時間

其實第二個案例就只是添加了 createRectTween,你能夠試着註釋 createRectTween;或者案例 2 中直接註釋 Hero 控件;看看它們的動畫效果是啥,想來你印象會更深。

所有代碼

    
  
  
  
   
   
            
   
   




















import 'package:flutter/material.dart';import 'package:flutter_travel/widgets/item_widget.dart';import 'package:flutter/scheduler.dart' show timeDilation;import 'dart:math' as math;const Tag = "tag"; ////惟一標記,先後兩個路由頁Hero的tag必須相同class HeroPage extends StatefulWidget { @override _HeroPageState createState() => _HeroPageState();}class _HeroPageState extends State<HeroPage> { @override void initState() { // TODO: implement initState super.initState(); } @override Widget build(BuildContext context) { timeDilation = 3; //動畫過渡時間 return Scaffold( appBar: AppBar( title: Text("Hero"), ), body: Container( margin: EdgeInsets.only(top: 30, right: 5, left: 5), child: Column( children: <Widget>[ Expanded( flex: 1, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Container( height: 100, alignment: Alignment.center, child: InkWell( child: Hero( tag: Tag, child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 80, height: 80, fit: BoxFit.cover, ), )), onTap: () {// Navigator.of(context).push(// new MaterialPageRoute(builder: (BuildContext context) {// return new HeroAnimationPage1();// })); Navigator.push(context, PageRouteBuilder(pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { return new FadeTransition( opacity: animation, child: Scaffold( appBar: AppBar( title: Text("Fade"), ), body: HeroAnimationRouteWithFade(), ), ); })); }, ), ), Container( height: 40, width: double.infinity, alignment: Alignment.center, margin: EdgeInsets.only(top: 30), child: Text( "下面的案例僅僅只是添加了RectTween", style: TextStyle(fontWeight: FontWeight.bold), ), ), ], )), Expanded( flex: 2, child: Container( alignment: Alignment.bottomCenter, margin: EdgeInsets.only(bottom: 100), child: _buildHero(context, 'assets/datas/empty.png', '空空如也'), )) ], ), ), ); } @override void dispose() { // TODO: implement dispose super.dispose(); }}class HeroAnimationPage1 extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Hero1"), ), body: Center( child: Container( margin: EdgeInsets.all(20), child: Hero( tag: Tag, //惟一標記,先後兩個路由頁Hero的tag必須相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 120, height: 120, fit: BoxFit.cover, ), ), ), ), )); }}class HeroAnimationRouteWithFade extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Container( margin: EdgeInsets.all(20), child: Hero( tag: "tag", //惟一標記,先後兩個路由頁Hero的tag必須相同 child: ClipOval( child: Image.asset( "assets/datas/night.jpg", width: 120, height: 120, fit: BoxFit.cover, ), ), ), ), ); }}/////////////////////////////////////華麗的分割線///////////////////////////////////const double kMinRadius = 40.0;const double kMaxRadius = 150.0;class Photo extends StatelessWidget { Photo({Key key, this.photo, this.color, this.onTap}) : super(key: key); final String photo; final Color color; final VoidCallback onTap; Widget build(BuildContext context) { return Material( color: Colors.grey.withOpacity(0.25), child: InkWell( onTap: onTap, child: Image.asset( photo, color: Colors.green.withOpacity(.8), fit: BoxFit.contain, ), ), ); }}RectTween _createRectTween(Rect begin, Rect end) { print("begin=${begin}\t end=${end}"); return MaterialRectCenterArcTween(begin: begin, end: end);}Widget _buildHero(BuildContext context, String imageName, String description) { return Container( width: kMinRadius * 2.0, height: kMinRadius * 2.0, child: Hero( createRectTween: _createRectTween, tag: imageName, child: RadialExpansion( maxRadius: kMaxRadius, child: Photo( photo: imageName, onTap: () { Navigator.of(context).push( PageRouteBuilder<void>( pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return FadeTransition( opacity: animation, child: _showDetailPage(context, imageName, description)); }, ), ); }, ), ), ), );}Widget _showDetailPage( BuildContext context, String imageName, String description) { return Container( color: Theme.of(context).canvasColor, child: Center( child: Card( elevation: 8.0, child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: kMaxRadius * 2.0, height: kMaxRadius * 2.0, child: Hero( createRectTween: _createRectTween, tag: imageName, child: RadialExpansion( maxRadius: kMaxRadius, child: Photo( photo: imageName, onTap: () { Navigator.of(context).pop(); }, ), ), ), ), Text( description, style: TextStyle( fontWeight: FontWeight.bold, ), textScaleFactor: 3.0, ), const SizedBox(height: 16.0), ], ), ), ), );}class RadialExpansion extends StatelessWidget { RadialExpansion({ Key key, this.maxRadius, this.child, }) : clipRectSize = 2.0 * (maxRadius / math.sqrt2), super(key: key); final double maxRadius; final clipRectSize; final Widget child; @override Widget build(BuildContext context) { return ClipOval( child: Center( child: SizedBox( width: clipRectSize, height: clipRectSize, child: ClipRect( child: child, ), ), ), ); }}


本文分享自微信公衆號 - Flutter學習簿(gh_d739155d3b2c)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索