Flutter-AnimatedList原始碼分析

Owen_Lee發表於2019-11-20

​ 最近倒騰Flutter,需要做列表的插入刪除動畫,用到了AnimatedList這個元件,也遇到一些問題,在這裡分析下原始碼以作備忘,不足之處希望大神指點

使用

先看下元件的建構函式

const AnimatedList({
    Key key,
    @required this.itemBuilder,
    this.initialItemCount = 0,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
    this.controller,
    this.primary,
    this.physics,
    this.shrinkWrap = false,
    this.padding,
  })
複製程式碼
  • itemBuilder :類似ListView的itemBuilder,為什麼說類似呢,因為AnimatedList裡實際渲染在螢幕上的item不一定都是從這裡構造出來的,當有刪除的操作動畫時,會有呼叫另一個回撥來生成,這個下面的分析會體現出來
  • initialItemCount:初始化的資料集數量,注意這個引數區別於ListView的itemCount,因為這是初始化的數量,之後外部再去重新改變這個變數是不會改變元件相應State裡的值的(widget銷燬,state可能會複用)
  • scrollDirection:佈局的方向
  • reverse:反向佈局
  • 剩餘屬性:都是直接賦值給內部包裹的ListView的

簡單使用如下:

...
final GlobalKey<AnimatedListState> _listKey = new GlobalKey<AnimatedListState>();
final List<String> _list = [];

Scaffold(
      backgroundColor: Colors.grey,
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          child: AnimatedList(
        key: _listKey,
        initialItemCount: _list.length,
        itemBuilder: buildItem,
      )),
    )
  /// 構建item
  Widget buildItem(
      BuildContext context, int index, Animation<double> animation) {...}
      
  /// 執行刪除動畫時需要替換原來位置item的元件
  Widget _buildRemovedItem(
      String item, BuildContext context, Animation<double> animation) {...}
      
  /// 增加一條資料
   void _insert(String item, [int index = 0]) {
    _list.insert(index, item);
    listKey.currentState.insertItem(index);
  }
  
  /// 刪除一條資料
   void _remove(int index) {
    var removedItem = _list.removeAt(index);
    listKey.currentState.removeItem(index,
          (BuildContext context, Animation<double> animation) {
        return _buildRemovedItem(removedItem, context, animation);
      })
  }
  
複製程式碼

GlobalKey的作用:

  1. widget的唯一標識,可防止父元件rebuild時,直接重建AnimatedList導致動畫效果消失
  2. 可從key裡直接獲取AnimatedListState,因為除了對資料來源進行操作外,還需要呼叫AnimatedListState的insertItem和removeItem方法

事實上AnimatedListState在初始化後,內部維護了_itemCount,所以當外部對資料集進行操作時,需要同步AnimatedListState

原始碼分析

看下元件對應的State

class AnimatedListState extends State<AnimatedList> with TickerProviderStateMixin<AnimatedList> {
  final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
  final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
  int _itemsCount = 0;

  @override
  void initState() {
    super.initState();
    _itemsCount = widget.initialItemCount;
  }

  @override
  void dispose() {
    for (_ActiveItem item in _incomingItems)
      item.controller.dispose();
    for (_ActiveItem item in _outgoingItems)
      item.controller.dispose();
    super.dispose();
  }
  
  ...
  
   @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: _itemBuilder,
      itemCount: _itemsCount,
      scrollDirection: widget.scrollDirection,
      reverse: widget.reverse,
      controller: widget.controller,
      primary: widget.primary,
      physics: widget.physics,
      shrinkWrap: widget.shrinkWrap,
      padding: widget.padding,
    );
  }
}
複製程式碼
  1. 看到build方法,這個元件就是封裝了ListView,裝飾器模式增強功能
  2. 混入了TickerProviderStateMixin,這個是做多動畫需要的
  3. _incomingItems:記錄正在執行插入動畫的item集合
  4. _outgoingItems:記錄正在執行刪除動畫的item集合
  5. _itemsCount:資料集的數量,顯然是在首次建立的時候進行了賦值,事實上在flutter渲染流程裡,widget是會不斷地被重新build生成新的例項的,而當滿足一定的條件時,如給widget設定了同一個GlobalKey等操作時,State是會被複用的,所以父元件的重新build時賦了一個新的count給initialItemCount是不生效的

重點來了~擴充套件功能的關鍵程式碼就在上面程式碼片段裡的...裡,我們接著看

insertItem

...
void insertItem(int index, { Duration duration = _kDuration }) {
	...
	
    final int itemIndex = _indexToItemIndex(index);
	
	...

    for (_ActiveItem item in _incomingItems) {
      if (item.itemIndex >= itemIndex)
        item.itemIndex += 1;
    }
    for (_ActiveItem item in _outgoingItems) {
      if (item.itemIndex >= itemIndex)
        item.itemIndex += 1;
    }

    final AnimationController controller = AnimationController(duration: duration, vsync: this);
    final _ActiveItem incomingItem = _ActiveItem.incoming(controller, itemIndex);
    setState(() {
      _incomingItems
        ..add(incomingItem)
        ..sort();
      _itemsCount += 1;
    });

    controller.forward().then<void>((_) {
      _removeActiveItemAt(_incomingItems, incomingItem.itemIndex).controller.dispose();
    });
  }
...
複製程式碼

當呼叫insertItem增加一個元素時

  1. 呼叫_indexToItemIndex(index),將外部資料來源集合傳入的index轉換成AnimatedListState內部實際的itemIndex(因為刪除動畫未播放完成 _itemCount的值是不會變的,所以會出現外部資料來源集合長度不一致的情況,需要做Index修正 )

     int _indexToItemIndex(int index) {
        int itemIndex = index;
        for (_ActiveItem item in _outgoingItems) {
          if (item.itemIndex <= itemIndex)
            itemIndex += 1;
          else
            break;
        }
        return itemIndex;
      }
    複製程式碼

    看的出來,這裡主要是對如果有正在刪除元素的動作情況下,對index修正(噹噹前傳入index>=刪除動畫的index時,即表明該傳入index對映在AnimatedListState裡認為的集合裡的位置應該要+1)

  2. 遍歷正在插入動畫的item集合,因為插入了個新元素,所以原本播放著動畫的item的itemIndex如果>=當前插入的index,則需要+1到正確的位置

  3. 將新增的index項封裝成_ActiveItem,新增到 _incomingItems裡,表示這個位置的item正在播放插入動畫,然後 _itemsCount+1

  4. 開啟動畫,動畫結束後將 item從_incomingItems裡清掉

細心的觀察會發現,第一步裡只對_outgoingItems集合進行了修正,沒有對 _incomingItem進行處理,這是因為新增的時候 _itemsCount += 1是在動畫開始前就設定了,而remove是在動畫結束後才會去減1的,之所以動畫結束後才減 _itemCount,主要是因為。。。減了widget就消失了!!!哪還有動畫

removeItem

void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) {
	...

    final int itemIndex = _indexToItemIndex(index);
	...

    final _ActiveItem incomingItem = _removeActiveItemAt(_incomingItems, itemIndex);
    final AnimationController controller = incomingItem?.controller
      ?? AnimationController(duration: duration, value: 1.0, vsync: this);
    final _ActiveItem outgoingItem = _ActiveItem.outgoing(controller, itemIndex, builder);
    setState(() {
      _outgoingItems
        ..add(outgoingItem)
        ..sort();
    });

    controller.reverse().then<void>((void value) {
      _removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex).controller.dispose();

      // Decrement the incoming and outgoing item indices to account
      // for the removal.
      for (_ActiveItem item in _incomingItems) {
        if (item.itemIndex > outgoingItem.itemIndex)
          item.itemIndex -= 1;
      }
      for (_ActiveItem item in _outgoingItems) {
        if (item.itemIndex > outgoingItem.itemIndex)
          item.itemIndex -= 1;
      }

      setState(() {
        _itemsCount -= 1;
      });
    });
  }
複製程式碼

刪除動畫,主要步驟如下:

  1. 同樣是對傳入的外部資料來源集合的index進行修正,呼叫_indexToItemIndex方法
  2. _removeActiveItemAt 判斷刪除的item此時是否正在播放插入動畫,有則取出AnimationController,無則新建動畫
  3. 將此刪除的item封裝進 _ActiveItem 並加入 _outgoingItems裡
  4. 對AnimationController進行reverse反向播放,即播放刪除動畫
  5. 動畫播放完畢後,對其餘正在播放動畫的item的index進行修正,即如果當前刪除的itemIndex < 正在播放動畫的item的index,則位置-1
  6. 最後將 _itemsCount數量-1

上述動作都是在對插入、刪除動作進行index、動畫的處理,當開啟動畫後,便會開始不斷地觸發build,而build方法裡則構造ListView,最終呼叫_itemBuilder方法

_itemBuilder

Widget _itemBuilder(BuildContext context, int itemIndex) {
    final _ActiveItem outgoingItem = _activeItemAt(_outgoingItems, itemIndex);
    if (outgoingItem != null)
      return outgoingItem.removedItemBuilder(context, outgoingItem.controller.view);

    final _ActiveItem incomingItem = _activeItemAt(_incomingItems, itemIndex);
    final Animation<double> animation = incomingItem?.controller?.view ?? kAlwaysCompleteAnimation;
    return widget.itemBuilder(context, _itemIndexToIndex(itemIndex), animation);
  }
複製程式碼

上述_itemBuilder方法,是直接賦值給ListView的itemBuilder屬性,用來構建檢視列表的每個item的widget的回撥

  1. 首先判斷回撥itemIndex對應的Item此時是否正在進行刪除動畫, _activeItemAt(_outgoingItems, itemIndex)返回封裝的 _ActiveItem,不為null則表示找到,此時應該呼叫removeItem方法傳入的AnimatedListRemovedItemBuilder回撥進行構建刪除顯示的Widget,之後構建下一個item

  2. 如果判斷此ItemIndex沒在刪除動畫集合裡,則再判斷是否是正在執行插入動畫的item,是則取出Animation,否則使用預設的Animation,最後回撥父widget傳入的函式,進行構建Widget

  3. _itemIndexToIndex(itemIndex):該方法和 _indexToItemIndex方法相反,它是將內部的itemIndex轉成外部資料來源集合相應的index(因為如果有正在執行刪除動畫的item則內外count會存在不一致)

    int _itemIndexToIndex(int itemIndex) {
        int index = itemIndex;
        for (_ActiveItem item in _outgoingItems) {
          assert(item.itemIndex != itemIndex);
          if (item.itemIndex < itemIndex)
            index -= 1;
          else
            break;
        }
        return index;
      }
    複製程式碼

    可以看出來,遍歷正在刪除動畫的item,如果此時的itemIndex大於它們,則需要-1才能修正回去

總結

AnimatedList主要是利用裝飾器模式對ListView進行了功能上的擴充套件,其在初始化後,內部對資料集的數量進行了維護以方便動畫的播放,需要注意的是:進行刪除動畫時,實際上資料來源集合的長度和內部_itemCount是會在一段時間記憶體在不一致的

亮點:

  1. 對itemIndex進行封裝處理,完全解耦資料來源
  2. 內部播放動畫的控制機制與外部完全解耦,外部只需告訴widget 插入展示和刪除展示的widget即可

坑:

  1. 對資料來源集合進行整個替換或一定範圍的更新,沒有有效支援

本文由Owen Lee原創,轉載請註明來源: juejin.im/post/5dd525…

相關文章