Flutter 入門指北(Part 10)之手勢處理和動畫

kukyxs發表於2019-04-14

該文已授權公眾號 「碼個蛋」,轉載請指明出處

Flutter 中,自帶手勢監聽的目前為止好像只有按鈕部件和一些 chip 部件,例如 Text 等部件需要實現手勢監聽,就需要藉助帶有監聽事件的部件來實現了,這節我們會講下 InkWellGestureDetector 來實現手勢的監聽。

####InkWell

在前面的一些例子中,小夥伴應該看到了好幾次 InkWell 這個部件,通過它我們可以實現對一些手勢的監聽,並實現 MD 的水波紋效果,舉個簡單的一個例子

InkWell(
  child: Text('點我...點我...我能響應點選手勢'),
  onTap: () => print('啊...我被點選了...')
),
複製程式碼

那麼當點選 Text 的時候就會響應點選事件,控制檯輸出日誌

我們還是老套路,分析下原始碼。Ctrl 點選 InkWell 來檢視原始碼(Android Studio 的操作,別的我不懂喔...),然後,「嗯...除了建構函式怎麼什麼都沒有???」那隻能看它的父類 InkResponse 了,在那之前,我們看下 InkWell 的說明

/// A rectangular area of a [Material] that responds to touch.
複製程式碼

InkWell 是在 MaterialDesign 風格下的一個用來響應觸控的矩形區域(注意加粗的文字,1.如果不是 MD 風格的部件下,你是不能用這個來做點選響應的;2.InkWell 是一塊矩形區域,如果你要的是圓形區域,8 好意思,不行!)

/// The [InkWell] widget must have a [Material] widget as an ancestor. The
/// [Material] widget is where the ink reactions are actually painted. This
/// matches the material design premise wherein the [Material] is what is
/// actually reacting to touches by spreading ink.```
複製程式碼

InkWell 必須要有一個 Material 風格的部件作為錨點,巴拉巴拉巴拉....再次強調必須要在 MD 風格下使用。

接下來看下 InkResponse

####InkResponse

const InkResponse({
    Key key,
    this.child, // 需要監聽的子部件
    // 一個 `GestureTapCallback` 型別引數,看下 `GestureTapCallback` 的定義,
    // `typedef GestureTapCallback = void Function();` 就是簡單的無參無返回型別引數
    // 監聽手指點選事件
    this.onTap,
    // 一個 `GestureTapDownCallback` 型別引數,需要 `TapDownDetails` 型別引數,
    // `TapDownDetails` 裡面有個 `Offset` 引數用於記錄點選的位置,監聽手指點選螢幕的事件
    this.onTapDown,
    // 同 `onTap` 表示點選事件取消監聽
    this.onTapCancel,
    // 同 `onTap` 表示雙擊事件監聽
    this.onDoubleTap,
    // 一個 `GestureLongPressCallback` 型別引數,也是無參無返回值,表示長按的監聽
    this.onLongPress,
    // 監聽高亮的變化,返回 `true` 表示往高亮變化,`false` 相反
    this.onHighlightChanged,
    // 是否需要裁剪區域,`InkWell` 該值為 `true`,會根據 `highlightShape` 裁剪
    this.containedInkWell = false,
    // 高亮的外形,`InkWell` 該值設定成 `BoxShape.rectangle`,所以是個矩形區域
    this.highlightShape = BoxShape.circle,
    this.radius, // 手指點下去的時候,出現水波紋的半徑
    this.borderRadius, // 點選時候外圈陰影的圓角半徑
    this.customBorder,
    this.highlightColor, // 高亮顏色
    this.splashColor, // 手指點下生成的水波顏色
    this.splashFactory, // 兩個值 `InkRipple.splashFactory` 和 `InkSplash.splashFactory`
    this.enableFeedback = true, // 檢測到手勢是否有反饋
    this.excludeFromSemantics = false,
  }) 
複製程式碼

所以一些簡單的觸控事件直接通過 InkWell 或者 InkResponse 就能夠實現,但是面臨一些比較複雜的手勢,就有點不太夠用了,我們需要通過 GestureDector 來進行處理

GestureDector

GestureDetector 也是一個部件,主要實現對各種手勢動作的監聽,其監聽事件檢視下面的表格

回撥方法 回撥描述
onTapDown 點選螢幕的手勢觸碰到螢幕時候觸發
onTapUp 點選螢幕抬手後觸發,點選結束
onTap 點選事件已經完成的時候觸發,和 onTapUp 幾乎同時
onTapCancel 點選未完成,被其它手勢取代的時候觸發
onDoubleTap 雙擊螢幕的時候觸發
onLongPress 長按螢幕的時候觸發
onLongPressUp 長按螢幕後抬手觸發
onVerticalDragDown 觸碰到螢幕,可能發生垂直方向移動觸發,onVerticalDrag 系列事件不會同 onHorizontalDrag 系列事件同時發生 ,如果發生了 onVerticalDrag 則接下來如何變化移動,都不會觸發 onHorizontalDrag 事件,除非取消後重新觸發。判斷兩者的關鍵是準備滑動的意圖,先發生橫向滑動則觸發 onHorizontalDrag 事件,否則 onVerticalDrag 事件。
onVerticalDragStart 觸碰到螢幕,並開始發生垂直方向的移動觸發
onVerticalDragUpdate 垂直方向移動的距離變化觸發
onVerticalDragEnd 抬手取消垂直方向移動的時候觸發
onVerticalDragCancel 觸發 onVerticalDragDown 但是沒有完成整個 onVerticalDrag 事件觸發
onHorizontalDrag 系列介紹省略同上...
onPanDown 觸碰到螢幕,準備滑動的時候觸發,onPan 系列回撥不可和 onVerticalDrag 或者 onHorizontalDrag 系列回撥同時設定
onPanStart 觸碰到螢幕,並開始滑動時候觸發
onPanUpdate 滑動位置發生改變的時候觸發
onPanEnd 滑動完成並抬手的時候觸發
onPanCancel 觸發 onPanDown 但是沒有完成整個 onPan 事件觸發
onScaleStart 兩個手指之間建立聯絡點觸發,初始縮放比例為 1.0
onScaleUpdate 手指距離發生變化,縮放比例也跟隨變化觸發
onScaleEnd 手指抬起,至間的聯絡斷開時候觸發

還有 onForcePress 系列事件,這個是根據對螢幕的擠壓力度進行觸發,需要達到某些定值才能觸發。GestureDetector 有個 behavior 屬性用於設定手勢監聽過程中的表現形式

  1. deferToChild 預設值,觸控到 child 的範圍才會觸發手勢,空白處不會觸發
  2. opaque 不透明模式,防止 background widget 接收到手勢
  3. translucent 半透明模式,剛好同 opaque 相反,允許 background widget 接收到手勢

介紹完了手勢,那就可以實際操練起來了,比如,實現一個跟隨手指運動的小方塊,先看下效果圖

gesture_01.gif

簡單的分析下,通過 Positioned 來設定小方塊的位置,根據 GestureDetectoronPanUpdate 修改 Positionedlefttop 值,當 onPanEnd 或者 onPanCancel 的時候設定為原點,那麼就可以有如圖的效果了

class GestureDemoPage extends StatefulWidget {
  @override
  _GestureDemoPageState createState() => _GestureDemoPageState();
}

class _GestureDemoPageState extends State<GestureDemoPage> {
  double left = 0.0;
  double top = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Gesture Demo'),
        ),
        body: Stack(
          alignment: Alignment.center,
          children: <Widget>[
            Positioned(child: Container(width: 50.0, height: 50.0, color: Colors.red), left: left, top: top),
            GestureDetector(
              behavior: HitTestBehavior.translucent,
              child: Container(
                  color: Colors.transparent,
                  width: MediaQuery.of(context).size.width - 10,
                  height: MediaQuery.of(context).size.height),
              onPanDown: (details) {
                setState(() {
                  left = details.globalPosition.dx;
                  top = details.globalPosition.dy;
                });
              },
              onPanUpdate: (details) {
                setState(() {
                  left = details.globalPosition.dx;
                  top = details.globalPosition.dy;
                });
              },
              onPanCancel: () {
                setState(() {
                  left = 0.0;
                  top = 0.0;
                });
              },
              onPanEnd: (details) {
                setState(() {
                  left = 0.0;
                  top = 0.0;
                });
              },
            )
          ],
        ));
  }
}
複製程式碼

如果說要實現一個放大縮小的方塊,就可以通過 onScaleUpdate 中獲取到的 details.scale 來設定方塊的寬高即可。這個比較簡單就留給小夥伴們自己實現效果了。

該部分程式碼檢視 gesture_main.dart 檔案

Animation 動畫

FlutterAnimation 是個抽象類,具體的實現需要看其子類 AnimationController,在這之前,先了解下 Animation 的一些方法和介紹。

  1. addListener / removeListener 新增的監聽用於監聽值的變化,remove 用於停止監聽

  2. addStatusListener / removeStatusListener 新增動畫狀態變化的監聽,remove 停止監聽,Animation 的狀態有 4 種:dismissed 動畫初始狀態,反向運動結束狀態,forward 動畫正向運動狀態,reverse 動畫反向運動狀態,completed 動畫正向運動結束狀態。

  3. drive 方法用於連線動畫,例如官方舉的例子,因為 AnimationController 是其子類,所以也擁有該方法

    Animation<Alignment> _alignment1 = _controller.drive(
         AlignmentTween(
           begin: Alignment.topLeft,
           end: Alignment.topRight,
         ),
       );
    複製程式碼

    上面的例子將 AnimationControllerAlignmentTween 結合成一個 Animation<Alignment> 動畫,當然 drive 可以結合多個動畫,例如

    Animation<Alignment> _alignment3 = _controller
           .drive(CurveTween(curve: Curves.easeIn))
           .drive(AlignmentTween(
             begin: Alignment.topLeft,
             end: Alignment.topRight,
           ));
    複製程式碼

因為 Animation 是抽象類,所以具體的還是需要通過 AnimationController 來實現。

####AnimationController

AnimationController({
    double value, // 設定初始的值
    this.duration, // 動畫的時長
    this.debugLabel, // 主要是用於 `toString` 方法中輸出資訊
    this.lowerBound = 0.0, // 最小範圍
    this.upperBound = 1.0, // 最大範圍
    // AnimationController 結束時候的行為,有 `normal` 和 `preserve` 兩個值可選
    this.animationBehavior = AnimationBehavior.normal, 
    // 這個屬性可以通過 with `SingleTickerProviderStateMixin` 
    // 或者 `TickerProviderStateMixin` 引入到 `State`,通過 `this` 指定
    @required TickerProvider vsync,
  })
複製程式碼

AnimationController 控制動畫的方法有這麼幾個

  1. forward 啟動動畫,和上面提到的 forward 狀態不一樣
  2. reverse 方向啟動動畫
  3. repeat 重複使動畫執行
  4. stop 停止動畫
  5. reset 重置動畫

大概瞭解了 AnimationController ,接下來通過一個實際的小例子來加深下印象,例如實現如下效果,點選開始動畫,結束後再點選反向動畫

animation01.gif

class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {
  AnimationController _animationController;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
        vsync: this, duration: Duration(milliseconds: 1000), lowerBound: 28.0, upperBound: 50.0);

    // 當動畫值發生變化的時候,重繪下 icon
    _animationController.addListener(() {
      setState(() {});
    });
  }

  @override
  void dispose() {
    // 一定要釋放資源
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Animation Demo'),
      ),
      body: Center(
        child: IconButton(
            icon: Icon(Icons.android, color: Colors.green[500], size: _animationController.value),
            onPressed: () {
              // 根據狀態執行不同動畫運動方式
              if (_animationController.status == AnimationStatus.completed)
                _animationController.reverse();
              else if (_animationController.status == AnimationStatus.dismissed)
                _animationController.forward();
            }),
      ),
    );
  }
}
複製程式碼

那麼如果要實現無限動畫呢,那就可以通過 addStatusListener 監聽動畫的狀態來執行,修改程式碼,在 initState 增加如下程式碼

_animationController.addStatusListener((status) {
      if (_animationController.status == AnimationStatus.completed)
        _animationController.reverse();  // 正向結束後開始反向
      else if (_animationController.status == AnimationStatus.dismissed) 
        _animationController.forward(); // 反向結束後開始正向
    });

    _animationController.forward(); // 啟動動畫
複製程式碼

Centerchild 替換成一個 Icon,因為上面已經啟動了動畫,所以不需要再用點選去啟動了,執行後就會無限放大縮小迴圈跑了。

在這個例子中,通過設定 AnimationControllerlowerBoundupperBound 實現了動畫的變化範圍,接下來,將通過 Tween 來實現動畫的變化範圍。先看下 Tween 的一些介紹。

Tween

/// A linear interpolation between a beginning and ending value.
///
/// [Tween] is useful if you want to interpolate across a range.
///
/// To use a [Tween] object with an animation, call the [Tween] object's
/// [animate] method and pass it the [Animation] object that you want to
/// modify.
///
/// You can chain [Tween] objects together using the [chain] method, so that a
/// single [Animation] object is configured by multiple [Tween] objects called
/// in succession. This is different than calling the [animate] method twice,
/// which results in two separate [Animation] objects, each configured with a
/// single [Tween].
複製程式碼

Tween 是一個線性插值(如果要修改運動的插值,可以通過 CurveTween 來修改),所以線上性變化的時候很有用

通過呼叫 Tweenanimate 方法生成一個 Animation(animate 一般傳入 AnimationController)

還可以通過 chain 方法將多個 Tween 結合到一起,這樣就不需要多次去呼叫 Tweenanimate 方法來生成動畫了,多次呼叫 animate 相當於使用了兩個分開的動畫來完成效果,但是 chain 結合到一起就是一個動畫過程

那麼對前面的動畫進行一些修改,通過 Tween 來控制值的變化

class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {
  AnimationController _animationController;
  Animation _scaleAnimation; // 動畫例項,用於修改值的大小

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 1000)); // 不通過 `lowerBound` 和 `upperBound` 設定範圍,改用 `Tween`

    // 當動畫值發生變化的時候,重繪下 icon
    _animationController.addListener(() {
      setState(() {});
    });

    _animationController.addStatusListener((status) {
      if (_animationController.status == AnimationStatus.completed)
        _animationController.reverse();
      else if (_animationController.status == AnimationStatus.dismissed)
        _animationController.forward();
    });

    // 通過 `Tween` 的 `animate` 生成一個 Animation
    // 再通過  Animation.value 進行值的修改
    _scaleAnimation = Tween(begin: 28.0, end: 50.0).animate(_animationController);
    _animationController.forward();
  }

  @override
  void dispose() {
    // 一定要釋放資源
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Animation Demo'),
      ),
      body: Center(
        // 通過動畫返回的值,修改圖示的大小
        child: Icon(Icons.favorite, color: Colors.red, size: _scaleAnimation.value),
      ),
    );
  }
}
複製程式碼

再次執行,還是能過達到之前的效果,那麼很多小夥伴肯定會問了,「**,加了那麼多程式碼,效果還是和以前的一樣,還不如不加...」好吧,我無法反駁,但是如果要實現多個動畫呢,那麼使用 Tween 就有優勢了,比如我們讓圖示大小變化的同時,顏色和位置也發生變化,只通過 AnimationController 要怎麼實現? 又比如說,運動的方式要先加速後減速,那隻通過 AnimationController 要如何實現?這些問題通過 Tween 就會非常方便解決,直接上程式碼

class _AnimationDemoPageState extends State<AnimationDemoPage> with TickerProviderStateMixin {
  AnimationController _animationController;
  Animation _scaleAnimation; // 用於控制圖示大小
  Animation<Color> _colorAnimation; // 控制圖示顏色
  Animation<Offset> _positionAnimation; // 控制圖示位置

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 2000));

    // 當動畫值發生變化的時候,重繪下 icon
    _animationController.addListener(() {
      setState(() {});
    });

    _animationController.addStatusListener((status) {
      if (_animationController.status == AnimationStatus.completed)
        _animationController.reverse();
      else if (_animationController.status == AnimationStatus.dismissed) _animationController.forward();
    });
    // 通過 `chain` 結合 `CurveTween` 修改動畫的運動方式,曲線型別可自行替換
    _scaleAnimation =
        Tween(begin: 28.0, end: 50.0).chain(CurveTween(curve: Curves.decelerate)).animate(_animationController);

    _colorAnimation = ColorTween(begin: Colors.red[200], end: Colors.red[900])
        .chain(CurveTween(curve: Curves.easeIn))
        .animate(_animationController);

    _positionAnimation = Tween(begin: Offset(100, 100), end: Offset(300, 300))
        .chain(CurveTween(curve: Curves.bounceInOut))
        .animate(_animationController);

    _animationController.forward(); // 啟動動畫
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Animation Demo'),
      ),
      body: Stack(
        children: <Widget>[
          Positioned(
            child: Icon(Icons.favorite, color: _colorAnimation.value, size: _scaleAnimation.value),
            left: _positionAnimation.value.dx,
            top: _positionAnimation.value.dy,
          )
        ],
      ),
    );
  }
}
複製程式碼

那麼最後的效果圖

animation02.gif

當然,Flutter 中已經實現的 Tween 還有很多,包括 BorderTweenTextStyleTweenThemeDataTween ..等等,實現的方式都是類似的,小夥伴們可以自己慢慢看。

AnimationWidget

在上面的例子中,都是通過 addListener 監聽動畫值變化,然後通過 setState 方法來實現重新整理效果。那麼 Flutter 也提供了一個部件 AnimationWidget 來實現動畫部件,就不需要一直監聽了,還是實現上面的例子

class RunningHeart extends AnimatedWidget {
  final List<Animation> animations; // 傳入動畫列表
  final AnimationController animationController; // 控制動畫

  RunningHeart({this.animations, this.animationController})
      // 對傳入的引數進行限制(當然你也可以不做限制)
      : assert(animations.length == 3),
        assert(animations[0] is Animation<Color>),
        assert(animations[1] is Animation<double>),
        assert(animations[2] is Animation<Offset>),
        super(listenable: animationController);

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned(
          // 之前的 animation 都通過 animations 引數傳入到 `AnimationWidget`
          child: Icon(Icons.favorite, color: animations[0].value, size: animations[1].value),
          left: animations[2].value.dx,
          top: animations[2].value.dy,
        )
      ],
    );
  }
}
複製程式碼

其實內部返回的部件和前面的是一樣的

接著對 _AnimationDemoPageState 類進行修改,註釋 initState 中的 _animationController.addListener 所有內容,然後將 body 屬性替換成新建的 RunningHeart 部件,記得傳入的動畫列表的順序

body: RunningHeart(
        animations: [_colorAnimation, _scaleAnimation, _positionAnimation],
        animationController: _animationController,
      )
複製程式碼

這樣就實現了剛才一樣的效果,並且沒有一直呼叫 setState 來重新整理。

該部分程式碼檢視 animation_main.dart 檔案

StaggeredAnimations

Flutter 還提供了交錯動畫,聽名字就可以知道,是按照時間軸,進行不同的動畫,並且由同個AnimationController 進行控制。因為沒有找到好的例子,原諒我直接搬官方的例子來講,官方交錯動畫 demo

在繼續看之前,先了解下 Interval

/// An [Interval] can be used to delay an animation. For example, a six second
/// animation that uses an [Interval] with its [begin] set to 0.5 and its [end]
/// set to 1.0 will essentially become a three-second animation that starts
/// three seconds later.
複製程式碼

Interval 用來延遲動畫,例如一個時長 6s 的動畫,通過 Interval 設定其 begin 引數為 0.5,end 引數設定為 1.0,那麼這個動畫就會變成 3s 的動畫,並且開始的時間延遲了 3s。

瞭解 Interval 功能後,就可以看下例項了,當然我們不和官方的 demo 一樣,中間加個旋轉動畫

class StaggeredAnim extends StatelessWidget {
  final AnimationController controller;
  final Animation<double> opacity;
  final Animation<double> width;
  final Animation<double> height;
  final Animation<EdgeInsets> padding;
  final Animation<BorderRadius> border;
  final Animation<Color> color;
  final Animation<double> rotate;

  StaggeredAnim({Key key, this.controller}):
        // widget 透明度
        opacity = Tween(begin: 0.0, end: 1.0)
            .animate(CurvedAnimation(parent: controller, curve: Interval(0.0, 0.1, curve: Curves.ease))),
        // widget 寬
        width = Tween(begin: 50.0, end: 150.0)
            .animate(CurvedAnimation(parent: controller, curve: Interval(0.1, 0.250, curve: Curves.ease))),
        // widget 高
        height = Tween(begin: 50.0, end: 150.0)
            .animate(CurvedAnimation(parent: controller, curve: Interval(0.25, 0.375, curve: Curves.ease))),
        // widget 底部距離
        padding = EdgeInsetsTween(begin: const EdgeInsets.only(top: 150.0), end: const EdgeInsets.only(top: .0))
            .animate(CurvedAnimation(parent: controller, curve: Interval(0.25, 0.375, curve: Curves.ease))),
        // widget 旋轉
        rotate = Tween(begin: 0.0, end: 0.25)
            .animate(CurvedAnimation(parent: controller, curve: Interval(0.375, 0.5, curve: Curves.ease))),
        // widget 外形
        border = BorderRadiusTween(begin: BorderRadius.circular(5.0), end: BorderRadius.circular(75.0))
            .animate(CurvedAnimation(parent: controller, curve: Interval(0.5, 0.75, curve: Curves.ease))),
        // widget 顏色
        color = ColorTween(begin: Colors.blue, end: Colors.orange)
            .animate(CurvedAnimation(parent: controller, curve: Interval(0.75, 1.0, curve: Curves.ease))),
        super(key: key);

  Widget _buildAnimWidget(BuildContext context, Widget child) {
    return Container(
      padding: padding.value,
      alignment: Alignment.center,
      // 旋轉變化
      child: RotationTransition(
        turns: rotate, // turns 表示當前動畫的值 * 360° 角度
        child: Opacity(
          opacity: opacity.value, // 透明度變化
          child: Container(
            width: width.value, // 寬度變化
            height: height.value, // 高度變化
            decoration: BoxDecoration(
                color: color.value, // 顏色變化
                border: Border.all(color: Colors.indigo[300], width: 3.0),
                borderRadius: border.value), // 外形變化
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    // AnimatedBuilder 繼承 AnimationWidget,用來快速構建動畫部件
    return AnimatedBuilder(animation: controller, builder: _buildAnimWidget);
  }
}
複製程式碼

然後修改 body 的引數,設定成我們的動畫,當點選的時候就會啟動動畫

	GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: _playAnim,
        child: Center(
          // 定義一個外層圈,能夠使動畫顯眼點
          child: Container(
            width: 300,
            height: 300,
            decoration: BoxDecoration(
                color: Colors.black.withOpacity(0.1), border: Border.all(color: Colors.black.withOpacity(0.5))),
            child: StaggeredAnim(controller: _controller),
          ),
        ),
      )
複製程式碼

看下最後的效果吧

staggered_anim.gif

該部分程式碼檢視 staggered_animation_main.dart 檔案

結束前,我們再講一種比較簡單的 Hreo 動畫,用來過渡用。

Hero

通過指定 Hero 中的 tag,在切換的時候 Hero 會尋找相同的 tag,並實現動畫,具體的實現邏輯,這裡可以推薦一篇文章 談一談Flutter中的共享元素動畫Hero,裡面寫的很詳細,就不造車輪了。當然這邊還是得提供個簡單的 demo 的,替換前面的 body 引數

body: Container(
        alignment: Alignment.center,
        child: InkWell(
          child: Hero(
            tag: 'hero_tag', // 這裡指定 tag
            child: Image.asset('images/ali.jpg', width: 100.0, height: 100.0),
          ),
          onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => HeroPage())),
        ),
      )
複製程式碼

然後建立 HeroPage 介面,當然也可以是個 Dialog,只要通過路由實現即可

class HeroPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        alignment: Alignment.center,
        child: InkWell(
          child: Hero(tag: 'hero_tag', child: Image.asset('images/ali.jpg', width: 200.0, height: 200.0)),
          onTap: () => Navigator.pop(context),
        ),
      ),
    );
  }
}
複製程式碼

看下最後的效果圖:

hero.gif

該部分程式碼檢視 animation_main.dart 檔案

這一部分講的比較多,小夥伴可以慢慢消化,下節我會盡量填下之前留下的狀態管理的坑。

最後程式碼的地址還是要的:

  1. 文章中涉及的程式碼:demos

  2. 基於郭神 cool weather 介面的一個專案,實現 BLoC 模式,實現狀態管理:flutter_weather

  3. 一個課程(當時買了想看下程式碼規範的,程式碼更新會比較慢,雖然是跟著課上的一些寫程式碼,但是還是做了自己的修改,很多地方看著不舒服,然後就改成自己的實現方式了):flutter_shop

如果對你有幫助的話,記得給個 Star,先謝過,你的認可就是支援我繼續寫下去的動力~

相關文章