Flutter-AnimatedList源码分析

​ 最近倒腾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原创,转载请注明来源:
https://www.jianshu.com/p/ae094b8ec058

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容