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

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

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,先謝過,你的認可就是支持我繼續寫下去的動力~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380