最近倒騰Flutter,須要作列表的插入刪除動畫,用到了AnimatedList這個組件,也遇到一些問題,在這裏分析下源碼以做備忘,不足之處但願大神指點bash
先看下組件的構造函數app
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,
})
複製代碼
簡單使用以下:ide
...
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的做用:函數
事實上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,
);
}
}
複製代碼
重點來了~擴展功能的關鍵代碼就在上面代碼片斷裏的...裏,咱接着看post
...
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增長一個元素時動畫
調用_indexToItemIndex(index),將外部數據源集合傳入的index轉換成AnimatedListState內部實際的itemIndex(由於刪除動畫未播放完成 _itemCount的值是不會變的,因此會出現外部數據源集合長度不一致的狀況,須要作Index修正 )ui
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)this
遍歷正在插入動畫的item集合,由於插入了個新元素,因此本來播放着動畫的item的itemIndex若是>=當前插入的index,則須要+1到正確的位置
將新增的index項封裝成_ActiveItem,添加到 _incomingItems裏,表示這個位置的item正在播放插入動畫,而後 _itemsCount+1
開啓動畫,動畫結束後將 item從_incomingItems裏清掉
細心的觀察會發現,第一步裏只對_outgoingItems集合進行了修正,沒有對 _incomingItem進行處理,這是由於新增的時候 _itemsCount += 1是在動畫開始前就設置了,而remove是在動畫結束後纔會去減1的,之因此動畫結束後才減 _itemCount,主要是由於。。。減了widget就消失了!!!哪還有動畫
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;
});
});
}
複製代碼
刪除動畫,主要步驟以下:
上述動做都是在對插入、刪除動做進行index、動畫的處理,當開啓動畫後,便會開始不斷地觸發build,而build方法裏則構造ListView,最終調用_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的回調
首先判斷回調itemIndex對應的Item此時是否正在進行刪除動畫, _activeItemAt(_outgoingItems, itemIndex)返回封裝的 _ActiveItem,不爲null則表示找到,此時應該調用removeItem方法傳入的AnimatedListRemovedItemBuilder回調進行構建刪除顯示的Widget,以後構建下一個item
若是判斷此ItemIndex沒在刪除動畫集合裏,則再判斷是不是正在執行插入動畫的item,是則取出Animation,不然使用默認的Animation,最後回調父widget傳入的函數,進行構建Widget
_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是會在一段時間內存在不一致的
亮點:
坑:
本文由Owen Lee原創,轉載請註明來源: juejin.im/post/5dd525…