Flutter互動實戰-即刻App探索頁下拉&拖拽效果

HitenDev發表於2019-03-04

Flutter最近比較熱門,但是Flutter成體系的文章並不多,前期避免不了踩坑;我這篇文章主要介紹如何使用Flutter實現一個比較複雜的手勢互動,順便分享一下我在使用Flutter過程中遇到的一些小坑,減少大家入坑;

先睹為快

本專案支援iosandroid平臺,效果如下

Flutter互動實戰-即刻App探索頁下拉&拖拽效果 Flutter互動實戰-即刻App探索頁下拉&拖拽效果 Flutter互動實戰-即刻App探索頁下拉&拖拽效果 Flutter互動實戰-即刻App探索頁下拉&拖拽效果

對了,順便分享一下生成gif的小竅門,建議用手機自帶錄屏功能匯出mp4檔案到電腦,然後電腦端用ffmpeg命令列處理,控制gif的質量和檔案大小,我的建議是解析度控制在270p,幀率在10左右;

互動分析

看文章的小夥伴最好能手持即刻App,親自體驗一下探索頁的互動,是黃色Logo黃色主題色的即刻,人稱‘黃即’;

Flutter互動實戰-即刻App探索頁下拉&拖拽效果

即刻App原版功能有卡片旋轉,卡片撤回和卡片自動移除,時間關係暫時沒有實現,但核心的功能都在;

從一個Android開發者的習慣來看待,這個互動可拆分內外兩層控制元件,外層我們需要一個整體下拉的控制元件,我稱為下拉控制元件;內層我們需要實現一個上、下、左、右四方向拖拽移動的控制元件,我們稱為卡片控制元件下拉控制元件卡片控制元件不僅要處理手勢,還需要處理子Widget的佈局;下面我再分析細節功能:

下拉控制元件:

  • 子控制元件從上到下豎直襬放,頂部選單預設隱藏在螢幕外
  • 下拉手勢所有子控制元件下移,選單視覺差效果
  • 支援點選自動展開、收起效果

卡片控制元件

  • 卡片層疊佈局,錯落有致
  • 最上層卡片支援手勢拖拽
  • 其他卡片響應拖拽小幅位移
  • 鬆手移除卡片

碼上入手

熱身

套用App開發伎倆,實現上面的互動無非就是控制元件佈局和手勢識別。當然Flutter開發也是這些套路,只不過萬物皆是Widget,在Flutter中常用的基本佈局有ColumnRowStack等,手勢識別有ListenerGestureDetectorRawGestureDetector等,這是本文重點講解的控制元件,不限於上面這幾個Widget,因為Flutter提供的Widget太多了,重點的控制元件需要牢記外,其他時候真是現用現查;

所以下面我們從佈局和手勢這兩個大的技術點,來一一擊破功能點;

佈局擺放

這裡所謂的佈局,包括Widget的尺寸大小和位置的控制,一般都是父Widget掌管子Widget的命運,Flutter就是一層一層Widget巢狀,不要擔心,下面從外到內具體案例講解;

下拉控制元件

首先我們要實現最外層佈局,效果是:子Widget豎直襬放,且最上面的Widget預設需要擺放在螢幕外;

Flutter互動實戰-即刻App探索頁下拉&拖拽效果

如上圖所示,紅色區域是螢幕範圍,header是頭部隱藏的選單佈局,content是卡片佈局的主體;

先說入的坑

豎直佈局我最先想到的是Column,我想要的效果是content高度和父Widget的高度一致,我首先想到是讓Expanded包裹content,結果是content的高度永遠等於Column高度減header高度,造成現象就是content高度不填充,或者是擠壓現象,如果繼續使用Colunm可能就得放棄Expanded,手動給content賦值高度,沒準是個辦法,但我不願意手動賦值content的高度,太不優雅了,最後果斷棄用Column

另一個問題是如何隱藏header,我想到兩種方案:

  1. 採用外層Transform包裹整個佈局,內層Transform包裹header,然後賦值內層dy = -headerHeight,隨著手勢下拉動態,並不改變headerTransform,而是改變最外層Transformdy
  2. 動態改變header高度,初始高度為0,隨著手勢下拉動態計算;

但是上面這兩種都有坑,第一種方式會影響控制元件的點選事件,onTap方法不會被回撥;第二種由於高度在不斷改變,會影響header內部子Widget的佈局,很難做視覺差的控制;

最終方案

最後採用Stack來佈局,通過Stack配合Positioned,實現header佈局在螢幕外,而且可以做到讓content佈局填充父Widget;

PullDragWidget

Widget build(BuildContext context) {
  return RawGestureDetector(
      behavior: HitTestBehavior.translucent,
      gestures: _contentGestures,
      child: Stack(
        children: <Widget>[
          Positioned(//content佈局
              top: _offsetY,
              bottom: -_offsetY,
              left: 0,
              right: 0,
              child: IgnorePointer(
                ignoring: _opened,
                child: widget.child,
              )),
          Positioned(////header佈局
              top: -widget.dragHeight + _offsetY,
              bottom: null,
              left: 0,
              right: 0,
              height: widget.dragHeight,
              child: _headerWidget()),
        ],
      ));
}
複製程式碼

首先解釋一下Positioned的基本用法,topbottomheight控制高度和位置,而且兩兩配合使用,topbottom可以理解成marginTop和marginBottom,height顧名思義是直接Widget的高度,如果top配置bottom,意味著高度等於parentHeight-top-bottom,如果top/bottom配合height使用,高度一般是固定的,當然topbottom是接受負數的;

再分析程式碼,首先_offsetY是下拉距離,是一個改變的量初始值為0,content需要設定top = _offsetYbottom = -_offsetY,改變的是上下位置,高度不會改變;同理,header是採用topheight控制,高度固定,只需要動態改變top即可;

用Flutter寫佈局真的很簡單,我極力推崇使用Stack佈局,因為它比較靈活,沒有太多的限制,用好Stack主要還得用好Positioned,學好它沒錯;

卡片控制元件

卡片實現的效果就是依次層疊,錯落有致,這個很容易想到Stack來實現,當然有了上面踩坑,用Stack算是很輕鬆了;

Flutter互動實戰-即刻App探索頁下拉&拖拽效果

重疊的效果使用Stack很簡單,錯落有致的效果實在起來可能性就比較多了,比如可以使用Positioned,也可以包裹Container改變margin或者padding,但是考慮到角度的旋轉,我選擇使用Transform,因為Transform不僅可以玩轉位移,還有角度和縮放等,其內部實際上是操作一個矩陣變換;Transform挺好用,但是在Transform多層巢狀的某些特殊情況下,會存在不響應onTap事件的情況,我想這應該是Transform的bug,拖拽事件暫時沒有發現問題,這個是不是bug有待確認,暫時不影響使用;

CardStackWidget

Widget build(BuildContext context) {
  if (widget.cardList == null || widget.cardList.length == 0) {
    return Container();
  }
  List<Widget> children = new List();
  int length = widget.cardList.length;
  int count = (length > widget.cardCount) ? widget.cardCount : length;
  for (int i = 0; i < count; i++) {
    double dx = i == 0 ? _totalDx : -_ratio * widget.offset;
    double dy = i == 0 ? _totalDy : _ratio * widget.offset;
    Widget cardWidget = _CardWidget(
      cardEntity: widget.cardList[i],
      position: i,
      dx: dx,
      dy: dy,
      offset: widget.offset,
    );
    if (i == 0) {
      cardWidget = RawGestureDetector(
        gestures: _cardGestures,
        behavior: HitTestBehavior.deferToChild,
        child: cardWidget,
      );
    }
    children.add(Container(
      child: cardWidget,
      alignment: Alignment.topCenter,
      padding: widget.cardPadding,
    ));
  }
  return Stack(
    children: children.reversed.toList(),
  );
}
複製程式碼

_CardWidget

Widget build(BuildContext context) {
  return AspectRatio(
    aspectRatio: 0.75,
    child: Transform(
        transform: Matrix4.translationValues(
            dx + (offset * position.toDouble()),
            dy + (-offset * position.toDouble()),
            0),
        child: ClipRRect(
          borderRadius: BorderRadius.circular(10),
          child: Stack(
            fit: StackFit.expand,
            children: <Widget>[
              Image.network(
                cardEntity.picUrl,
                fit: BoxFit.cover,
              ),
              Container(color: const Color(0x5a000000)),
              Container(
                margin: EdgeInsets.all(20),
                alignment: Alignment.center,
                child: Text(
                  cardEntity.text,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                      letterSpacing: 2,
                      fontSize: 22,
                      color: Colors.white,
                      fontWeight: FontWeight.bold),
                  maxLines: 4,
                ),
              )
            ],
          ),
        )),
  );
}
複製程式碼

簡單總結一下卡片佈局程式碼,CardStackWidget是管理卡片Stack的父控制元件,負責對每個卡片進行佈局,_CardWidget是對單獨卡片內部進行佈局,總體來說沒有什麼難點,細節控制邏輯是在對上層_CardWidget和底層_CardWidget偏移量的計算;

佈局的內容就講這麼多,整體來說還是比較簡單,所謂的有些坑也不一定算是坑,只是不適應某些應用場景罷了;

手勢識別

Flutter手勢識別最常用的是ListenerGestureDetector這兩個Widget,其中Listener主要針對原始觸控點進行處理,GestureDetector已經對原始觸控點加工成了不同的手勢;這兩個類的方法介紹如下;

Listener

Listener({
  Key key,
  this.onPointerDown, //手指按下回撥
  this.onPointerMove, //手指移動回撥
  this.onPointerUp,//手指抬起回撥
  this.onPointerCancel,//觸控事件取消回撥
  this.behavior = HitTestBehavior.deferToChild, //在命中測試期間如何表現
  Widget child
})
複製程式碼

GestureDetector手勢回撥:

Property/Callback Description
onTapDown 使用者每次和螢幕互動時都會被呼叫
onTapUp 使用者停止觸控螢幕時觸發
onTap 短暫觸控螢幕時觸發
onTapCancel 使用者觸控了螢幕,但是沒有完成Tap的動作時觸發
onDoubleTap 使用者在短時間內觸控了螢幕兩次
onLongPress 使用者觸控螢幕時間超過500ms時觸發
onVerticalDragDown 當一個觸控點開始跟螢幕互動,同時在垂直方向上移動時觸發
onVerticalDragStart 當觸控點開始在垂直方向上移動時觸發
onVerticalDragUpdate 螢幕上的觸控點位置每次改變時,都會觸發這個回撥
onVerticalDragEnd 當使用者停止移動,這個拖拽操作就被認為是完成了,就會觸發這個回撥
onVerticalDragCancel 使用者突然停止拖拽時觸發
onHorizontalDragDown 當一個觸控點開始跟螢幕互動,同時在水平方向上移動時觸發
onHorizontalDragStart 當觸控點開始在水平方向上移動時觸發
onHorizontalDragUpdate 螢幕上的觸控點位置每次改變時,都會觸發這個回撥
onHorizontalDragEnd 水平拖拽結束時觸發
onHorizontalDragCancel onHorizontalDragDown沒有成功完成時觸發
onPanDown 當觸控點開始跟螢幕互動時觸發
onPanStart 當觸控點開始移動時觸發
onPanUpdate 螢幕上的觸控點位置每次改變時,都會觸發這個回撥
onPanEnd pan操作完成時觸發
onScaleStart 觸控點開始跟螢幕互動時觸發,同時會建立一個焦點為1.0
onScaleUpdate 跟螢幕互動時觸發,同時會標示一個新的焦點
onScaleEnd 觸控點不再跟螢幕有任何互動,同時也表示這個scale手勢完成

ListenerGestureDetector如何抉擇,首先GestureDetector是基於Listener封裝,它解決了大部分手勢衝突,我們使用GestureDetector就夠用了,但是GestureDetector不是萬能的,必要時候需要自定義RawGestureDetector

另外一個很重要的概念,Flutter手勢事件是一個從內Widget向外Widget的冒泡機制,假設內外Widget同時監聽豎直方向的拖拽事件onVerticalDragUpdate,往往都是內層控制元件獲得事件,外層事件被動取消;這樣的概念和Android父佈局攔截機制就完全不同了;

雖然Flutter沒有外層攔截機制,但是似乎還有一線希望,那就是IgnorePointerAbsorbPointerWidget,這倆哥們可以忽略或者阻止子Widget樹不響應Event;

手勢分析

基本原理介紹完了,接下來分析案例互動,上面說了我把整體佈局拆分成了下拉控制元件和卡片控制元件,分析即刻App的拖拽的行為:當下拉控制元件沒有展開下拉選單時,卡片控制元件是可以響應上、左、右三個方向的手勢,下拉控制元件只響應一個向下方向的手勢;當下拉選單展開時,卡片不能響應任何手勢,下拉控制元件可以響應豎直方向的所有事件;

Flutter互動實戰-即刻App探索頁下拉&拖拽效果

上圖更加形象解釋兩種狀態下的手勢響應,下拉控制元件是父Widget,卡片控制元件是子Widget,由於子Widget能優先響手勢,所以在初始階段,我們不能讓子Widget響應向下的手勢;

由於GestureDetector只封裝水平和豎直方向的手勢,且兩種手勢不能同時使用,我們從GestureDetector原始碼來看,能不能封裝一個監聽不同四個方向的手勢,;

GestureDetector

final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

if (onVerticalDragDown != null ||
    onVerticalDragStart != null ||
    onVerticalDragUpdate != null ||
    onVerticalDragEnd != null ||
    onVerticalDragCancel != null) {
  gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
    () => VerticalDragGestureRecognizer(debugOwner: this),
    (VerticalDragGestureRecognizer instance) {
      instance
        ..onDown = onVerticalDragDown
        ..onStart = onVerticalDragStart
        ..onUpdate = onVerticalDragUpdate
        ..onEnd = onVerticalDragEnd
        ..onCancel = onVerticalDragCancel;
    },
  );
}

return RawGestureDetector(
  gestures: gestures,
  behavior: behavior,
  excludeFromSemantics: excludeFromSemantics,
  child: child,
);
複製程式碼

GestureDetector最終返回的是RawGestureDetector,其中gestures是一個map,豎直方向的手勢在VerticalDragGestureRecognizer這個類;

VerticalDragGestureRecognizer

class VerticalDragGestureRecognizer extends DragGestureRecognizer {
  /// Create a gesture recognizer for interactions in the vertical axis.
  VerticalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);

  @override
  bool _isFlingGesture(VelocityEstimate estimate) {
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
    final double minDistance = minFlingDistance ?? kTouchSlop;
    return estimate.pixelsPerSecond.dy.abs() > minVelocity && estimate.offset.dy.abs() > minDistance;
  }

  @override
  bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dy.abs() > kTouchSlop;

  @override
  Offset _getDeltaForDetails(Offset delta) => Offset(0.0, delta.dy);

  @override
  double _getPrimaryValueFromOffset(Offset value) => value.dy;

  @override
  String get debugDescription => 'vertical drag';
}
複製程式碼

VerticalDragGestureRecognizer繼承DragGestureRecognizer,大部分邏輯都在DragGestureRecognizer中,我們只關注重寫的方法:

  • _hasSufficientPendingDragDeltaToAccept方法是關鍵邏輯,控制是否接受該拖拽手勢
  • _getDeltaForDetails返回拖拽進度的dx、dy偏移量
  • _getPrimaryValueFromOffset返回單方向手勢value,不同方向(同時擁有水平和豎直)的可以傳null
  • _isFlingGesture是否該手勢的Fling行為

自定義DragGestureRecognizer

想實現接受三個方向的手勢,自定義DragGestureRecognizer是一個好的思路;我希望接受上、下、左、右四個方向的引數,根據引數不同監聽不同的手勢行為,照葫蘆畫瓢自定義一個接受方向的GestureRecognizer

DirectionGestureRecognizer

class DirectionGestureRecognizer extends _DragGestureRecognizer {
  int direction;
  //接受中途變動
  ChangeGestureDirection changeGestureDirection;
  //不同方向
  static int left = 1 << 1;
  static int right = 1 << 2;
  static int up = 1 << 3;
  static int down = 1 << 4;
  static int all = left | right | up | down;

  DirectionGestureRecognizer(this.direction,
      {Object debugOwner})
      : super(debugOwner: debugOwner);

  @override
  bool _isFlingGesture(VelocityEstimate estimate) {
    if (changeGestureDirection != null) {
      direction = changeGestureDirection();
    }
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
    final double minDistance = minFlingDistance ?? kTouchSlop;
    if (_hasAll) {
      return estimate.pixelsPerSecond.distanceSquared > minVelocity &&
          estimate.offset.distanceSquared > minDistance;
    } else {
      bool result = false;
      if (_hasVertical) {
        result |= estimate.pixelsPerSecond.dy.abs() > minVelocity &&
            estimate.offset.dy.abs() > minDistance;
      }
      if (_hasHorizontal) {
        result |= estimate.pixelsPerSecond.dx.abs() > minVelocity &&
            estimate.offset.dx.abs() > minDistance;
      }
      return result;
    }
  }

  bool get _hasLeft => _has(DirectionGestureRecognizer.left);

  bool get _hasRight => _has(DirectionGestureRecognizer.right);

  bool get _hasUp => _has(DirectionGestureRecognizer.up);

  bool get _hasDown => _has(DirectionGestureRecognizer.down);
  bool get _hasHorizontal => _hasLeft || _hasRight;
  bool get _hasVertical => _hasUp || _hasDown;

  bool get _hasAll => _hasLeft && _hasRight && _hasUp && _hasDown;

  bool _has(int flag) {
    return (direction & flag) != 0;
  }

  @override
  bool get _hasSufficientPendingDragDeltaToAccept {
    if (changeGestureDirection != null) {
      direction = changeGestureDirection();
    }
    // if (_hasAll) {
    //   return _pendingDragOffset.distance > kPanSlop;
    // }
    bool result = false;
    if (_hasUp) {
      result |= _pendingDragOffset.dy < -kTouchSlop;
    }
    if (_hasDown) {
      result |= _pendingDragOffset.dy > kTouchSlop;
    }
    if (_hasLeft) {
      result |= _pendingDragOffset.dx < -kTouchSlop;
    }
    if (_hasRight) {
      result |= _pendingDragOffset.dx > kTouchSlop;
    }
    return result;
  }

  @override
  Offset _getDeltaForDetails(Offset delta) {
    if (_hasAll || (_hasVertical && _hasHorizontal)) {
      return delta;
    }

    double dx = delta.dx;
    double dy = delta.dy;

    if (_hasVertical) {
      dx = 0;
    }
    if (_hasHorizontal) {
      dy = 0;
    }
    Offset offset = Offset(dx, dy);
    return offset;
  }

  @override
  double _getPrimaryValueFromOffset(Offset value) {
    return null;
  }

  @override
  String get debugDescription => 'orientation_' + direction.toString();
}
複製程式碼

由於DragGestureRecognizer的很多方法是私有的,想重新只能copy一份程式碼出來,然後重寫主要的方法,根據不同入參處理不同的手勢邏輯;

注意事項

敲黑板了,在自定義DragGestureRecognizer時:_getDeltaForDetails返回值表示dxdy的偏移量,在只存在水平或者只存在豎直方向的情況下,需要將另一個方向的dxdy置0;

當前Widget樹有且只存在一個手勢時,手勢判斷的邏輯_hasSufficientPendingDragDeltaToAccept可能不會被呼叫,這時候一定要重寫_getDeltaForDetails控制返回dxdy

如何使用

自定義的DirectionGestureRecognizer可以配置leftrightupdown四個方向的手勢,而且支援不同方向的組合;

比如我們只想監聽豎直向下方向,就建立DirectionGestureRecognizer(DirectionGestureRecognizer.down)的手勢識別;

想監聽上、左、右的手勢,建立DirectionGestureRecognizer(DirectionGestureRecognizer.left | DirectionGestureRecognizer.right | DirectionGestureRecognizer.up)的手勢識別;

DirectionGestureRecognizer就像一把磨刀石,刀已經磨鋒利,砍材就很輕鬆了,下面進行控制元件的手勢實現;

下拉控制元件手勢

PullDragWidget

_contentGestures = {
//向下的手勢
  DirectionGestureRecognizer:
      GestureRecognizerFactoryWithHandlers<DirectionGestureRecognizer>(
          () => DirectionGestureRecognizer(DirectionGestureRecognizer.down),
          (instance) {
    instance.onDown = _onDragDown;
    instance.onStart = _onDragStart;
    instance.onUpdate = _onDragUpdate;
    instance.onCancel = _onDragCancel;
    instance.onEnd = _onDragEnd;
  }),
  //點選的手勢
  TapGestureRecognizer:
      GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
          () => TapGestureRecognizer(), (instance) {
    instance.onTap = _onContentTap;
  })
};

Widget build(BuildContext context) {
  return RawGestureDetector(//返回RawGestureDetector
      behavior: HitTestBehavior.translucent,
      gestures: _contentGestures,//手勢在此
      child: Stack(
        children: <Widget>[
          Positioned(
              top: _offsetY,
              bottom: -_offsetY,
              left: 0,
              right: 0,
              child: IgnorePointer(
                ignoring: _opened,
                child: widget.child,
              )),
          Positioned(
              top: -widget.dragHeight + _offsetY,
              bottom: null,
              left: 0,
              right: 0,
              height: widget.dragHeight,
              child: _headerWidget()),
        ],
      ));
}
複製程式碼

PullDragWidget是下拉拖拽控制元件,根Widget是一個RawGestureDetector用來監聽手勢,其中gestures支援向下拖拽和點選兩個手勢;當下拉控制元件處於_opened狀態說header已經拉下來,此時配合IgnorePointer,禁用子Widget所有的事件監聽,自然內部的卡片就響應不了任何事件;

卡片控制元件手勢

同下拉控制元件一樣,卡片控制元件只需要監聽其餘三個方向的手勢,即可完成任務:

CardStackWidget

_cardGestures = {
  DirectionGestureRecognizer://監聽上左右三個方向
      GestureRecognizerFactoryWithHandlers<DirectionGestureRecognizer>(
          () => DirectionGestureRecognizer(DirectionGestureRecognizer.left |
              DirectionGestureRecognizer.right |
              DirectionGestureRecognizer.up), (instance) {
    instance.onDown = _onPanDown;
    instance.onUpdate = _onPanUpdate;
    instance.onEnd = _onPanEnd;
  }),
  TapGestureRecognizer:
      GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
          () => TapGestureRecognizer(), (instance) {
    instance.onTap = _onCardTap;
  })
};
複製程式碼

手勢答疑

  • 為什麼不用 onPanDown onPanUpdate onPanEnd 來拖動?

這是掘金評論提的問題,我解答一下:在GestureDetector中有Pan手勢和Drag手勢,這兩個手勢都能用處拖拽的場景,但不同的是Drag手勢僅限於水平豎直方向的監聽,Pan手勢不約束方向任意方向都能監聽,除此之外觸發條件也不一致,Pan手勢的觸發條件是滑動動螢幕的距離distance大於kTouchSlop*2Drag手勢的觸發條件是dx或者dy大於kTouchSlopdxdydistance形成勾股定理的三個邊長;假設同樣在監聽豎直滑動這種場景,VerticalDrag總是比Pan先觸發;如果下拉控制元件用VerticalDrag卡片控制元件用Pan,下拉控制元件會優先獲取向上的拖拽,卡片控制元件就會失去向上拖拽的機會,這就實現不了互動了,退一步即使Pan的觸發條件跟VerticalDrag一樣,由於Flutter的事件傳遞是從內到外的,這會導致外層下拉控制元件完全失去響應機會。以上我的個人理解,如有誤導還請大佬評論指正。

手勢小結

分析Flutter手勢冒泡的特性,父Widget既沒有響應事件的優先權,也沒有監聽單獨方向(leftrightupdown)的手勢,只能自己想辦法自定義GestureRecognizer,把原本VerticalHorizontal兩個方向的手勢識別擴充套件成leftrightupdown四個方向,區分開會產生衝突的手勢;

當然也可能有其他的方案來實現該互動的手勢識別,條條大路通羅馬,我只是拋磚引玉,大家有好的方案可以積極留言提出寶貴意見;

總結

知識點

由於篇幅有限並沒有介紹完該互動的所有內容,深表遺憾,總結歸納一下程式碼中用到的知識點:

  • ColumnRowExpandedStackPositionedTransform等Widget的使用;
  • GestureDetectorRawGestureDetectorIgnorePointer等Widget的使用;
  • 自定義GestureRecognizer實現自定義手勢識別;
  • AnimationControllerTween等動畫的使用;
  • EventBus的使用;

最後

上面章節主要介紹在當前場景下用Flutter佈局和手勢的實戰技巧,其中更深層次手勢競技和分發的原始碼級分析,有機會再做深入學習和分享;

另外本篇並不是循序漸進的零基礎入門,對剛接觸的同學可能感覺有點懵,但是沒有關係,建議你clone一份程式碼跑起來效果,沒準就能提起自己學習的興趣;

最最後,本篇所有程式碼都是開源的,你的點贊是對我最大的鼓勵。

專案地址:github.com/HitenDev/Fl…

相關文章