小菜上周學習了 AnimatedSwitcher 隐式動畫
,發覺 AnimatedSwitcher 簡單而強大,可以用作自定義跑馬燈或無限輪播的 Banner,小菜今天以 AnimatedSwitcher 為基礎嘗試一個極簡跑馬燈 ACEMarquee;
實作跑馬燈效果主要是處理如下幾點:
- 跑馬燈各 item 不唯一,可根據需求自定義;
- 跑馬燈動畫效果,進入時和退出時動畫相反,整體形成一個無限循環;
- 跑馬燈各 item 添加點選回調事件;
源碼分析
ACEMarquee(
{Key key,
@required this.children, // 跑馬燈展示 item
this.duration, // 動畫時長
this.direction, // 動畫方向
this.onItemTap}); // item 點選回調
小菜自定義的跑馬燈參數簡單,其中 direction 為動畫方向,分别為 AxisDirection.left 右進左出 / AxisDirection.right 左進右出 / AxisDirection.up 下進上出 / AxisDirection.down 上進下出;
案例嘗試
1. 自定義 item
依據日常需求,跑馬燈過程中,item 可能會有差異,小菜采用的是 AnimatedSwitcher 方式,可以讓使用者随意傳遞 item Widget;其中需要注意的是小菜将使用者傳遞的 item 外層嵌套了一層帶有 Key 的 Container,保證每個 item 的 Key 值 不同;否則 AnimatedSwitcher 動畫不能正常執行;
Widget _itemWid(direction) {
if (_children != null) {
_children.clear();
}
for (int i = 0; i < widget.children.length; i++) {
_children.add(Container(key: ValueKey<int>(i), child: widget.children[i]));
}
return SizedBox(
child: AnimatedSwitcher(
duration: widget.duration ?? Duration(milliseconds: 1500),
child: _children[_index]));
}
return Container(
child: ACEMarquee(children: <Widget>[
Container(height: 100, color: Colors.orange.withOpacity(0.4)),
Container(height: 100, color: Colors.purpleAccent.withOpacity(0.4)),
Container(height: 100, color: Colors.redAccent.withOpacity(0.4)),
Container(height: 100, color: Colors.blueGrey.withOpacity(0.4))
], duration: Duration(milliseconds: 2000)));

2. 循環動畫
小菜通過設定 transitionBuilder 改變平移動畫進入和退出方向,剛開始嘗試時小菜通過設定 SlideTransition 的 (animation.status == AnimationStatus.dismissed) 進行區分,但是在 setState 之後動畫會重新進入,導緻不連貫;之後小菜通過繼承 AnimatedWidget 并設定 (position.status == AnimationStatus.reverse) 進行入場和出場動畫區分;
小菜在測試過程中發現 AnimatedSwitcher 動畫過程中在動畫過程中入場動畫和出場動畫均會完全展示,小菜靈機一動通過 ClipRRect 隻是展示中間固定大小 AnimatedSwitcher;
class ACEMarqueeTransition extends AnimatedWidget {
final bool transformHitTests;
final Widget child;
//退場(出)方向
final AxisDirection direction;
Tween<Offset> _tween;
ACEMarqueeTransition({
Key key,
@required Animation<double> position,
this.transformHitTests = true,
this.direction = AxisDirection.down,
this.child,
}) : assert(position != null),
super(key: key, listenable: position) {
// 偏移在内部處理
switch (direction) {
case AxisDirection.up:
_tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
break;
case AxisDirection.right:
_tween = Tween(begin: Offset(-1, 0), end: Offset(0, 0));
break;
case AxisDirection.down:
_tween = Tween(begin: Offset(0, -1), end: Offset(0, 0));
break;
case AxisDirection.left:
_tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
break;
}
}
Animation<double> get position => listenable;
@override
Widget build(BuildContext context) {
Offset offset = _tween.evaluate(position);
if (position.status == AnimationStatus.reverse) {
switch (direction) {
case AxisDirection.up:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.right:
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.down:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.left:
offset = Offset(-offset.dx, offset.dy);
break;
}
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child);
}
}
3. 添加點選回調
跑馬燈在動畫過程中允許使用者随意點選,包括滑動一部分時,前後兩個 item 均可正常點選,小菜添加了 onItemTap Function 監聽;
Widget _itemWid(direction) {
if (_children != null) {
_children.clear();
}
for (int i = 0; i < widget.children.length; i++) {
_children.add(Container(
key: ValueKey<int>(i),
child: GestureDetector(
child: widget.children[I],
onTap: () => widget.onItemTap != null ? widget.onItemTap(i) : null)));
}
return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(1.0)),
child: SizedBox(
child: AnimatedSwitcher(
duration: widget.duration ?? Duration(milliseconds: 1500),
child: _children[_index],
transitionBuilder: (Widget child, Animation<double> animation) {
return ACEMarqueeTransition(
child: child,
direction: widget.direction,
position: animation);
})));
}
小菜自定義的 ACEMarquee 跑馬燈還很不成熟,動畫簡單,如有錯誤請多多指導!
來源: 阿策小和尚