此圖與正文無關,只是爲了好看git
這段時間一直在學習 Flutter,在 dribble 上看到一張導航欄設計圖,就是下面這張,感受非常喜歡,因而思考着如何在 Flutter 中實現這個效果。github
設計圖做者:Lukáš Straňákcanvas
通過一番研究,大致上算是實現了效果(有些地方仍是須要改進的),以下:app
這篇文章和你們分享一下實現過程,一塊兒交流、學習。ide
實現這個效果主要用到了 AnimationController
和 CustomPaint
,切換導航時進行從新繪製。學習
首先搭建一下整個頁面的骨架:動畫
class FloatNavigator extends StatefulWidget {
@override
_FloatNavigatorState createState() => _FloatNavigatorState();
}
class _FloatNavigatorState extends State<FloatNavigator> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return Container(
child: Stack(children: [
Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0.0,
title: Text('Float Navigator'),
centerTitle: true,
),
backgroundColor: Color(0xFFFF0035),
),
Positioned(
bottom: 0.0,
child: Container(
width: width,
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
//浮動圖標
//全部圖標
],
),
),
)
]),
);
}
}
複製代碼
這裏將圖中的導航分紅兩個部分,一個是浮動圖標,另外一個是全部圖標,浮動圖標在點擊的時候會移動到全部圖標中對應圖標的位置,而全部圖標上的圓弧狀缺口也會一塊兒移動。ui
接下來,在 _FloatNavigatorState
定義一些變量,以供使用:this
int _activeIndex = 0; //激活項
double _height = 48.0; //導航欄高度
double _floatRadius; //懸浮圖標半徑
double _moveTween = 0.0; //移動補間
double _padding = 10.0; //浮動圖標與圓弧之間的間隙
AnimationController _animationController; //動畫控制器
Animation<double> _moveAnimation; //移動動畫
List _navs = [
Icons.search,
Icons.ondemand_video,
Icons.music_video,
Icons.insert_comment,
Icons.person
]; //導航項
複製代碼
接着在 initState
中對一些變量作初始化:spa
@override
void initState() {
_floatRadius = _height * 2 / 3;
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 400));
super.initState();
}
複製代碼
這裏我將懸浮圖標的半徑設置爲導航欄高度的三分之二,動畫時長設置爲 400 毫秒,固然這裏面的參數都是能夠改動的。
接着,實現懸浮圖標:
//懸浮圖標
Positioned(
top: _animationController.value <= 0.5
? (_animationController.value * _height * _padding / 2) -
_floatRadius / 3 * 2
: (1 - _animationController.value) *
_height *
_padding /
2 -
_floatRadius / 3 * 2,
left: _moveTween * singleWidth +
(singleWidth - _floatRadius) / 2 -
_padding / 2,
child: DecoratedBox(
decoration:
ShapeDecoration(shape: CircleBorder(), shadows: [
BoxShadow( //陰影效果
blurRadius: _padding / 2,
offset: Offset(0, _padding / 2),
spreadRadius: 0,
color: Colors.black26),
]),
child: CircleAvatar(
radius: _floatRadius - _padding, //浮動圖標和圓弧之間設置10pixel間隙
backgroundColor: Colors.white,
child: Icon(_navs[_activeIndex], color: Colors.black)),
),
)
複製代碼
這裏的 top
值看上去很複雜,但實際上並沒什麼特別的,只是爲了讓懸浮圖標上下移動而已,_animationController
產生的值爲 0.0 到 1.0,所以,這裏判斷若是小於等於 0.5,就讓圖標向下移動,大於 0.5 則向上移動(移動距離能夠隨意修改)。
left
作橫向移動,這裏使用的是 _moveTween
,由於移動的距離是 singleWidth
的倍數(固然最終移動距離還要減去半徑及間隙,這裏的倍數是指列如從索引 0 移動到索引 3 這之間途徑的導航項長度)。
再向下就是重頭戲了,全部圖標的繪製:
CustomPaint(
child: SizedBox(
height: _height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: _navs
.asMap()
.map((i, v) => MapEntry(
i,
GestureDetector(
child: Icon(v,
color: _activeIndex == i
? Colors.transparent
: Colors.grey),
onTap: () {
_switchNav(i);
},
)))
.values
.toList(),
),
),
painter: ArcPainter(
navCount: _navs.length,
moveTween: _moveTween,
padding: _padding),
)
複製代碼
這裏須要用到索引來肯定每次點擊的是第幾個導航,因此用到了 asMap
和 MapEntry
,ArcPainter
就是用來繪製背景的,來看一下繪製背景的實現(不要慌,_switchNav
方法我會在後面解釋的):
//繪製圓弧背景
class ArcPainter extends CustomPainter {
final int navCount; //導航總數
final double moveTween; //移動補間
final double padding; //間隙
ArcPainter({this.navCount, this.moveTween, this.padding});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = (Colors.white)
..style = PaintingStyle.stroke; //畫筆
double width = size.width; //導航欄總寬度,即canvas寬度
double singleWidth = width / navCount; //單個導航項寬度
double height = size.height; //導航欄高度,即canvas高度
double arcRadius = height * 2 / 3; //圓弧半徑
double restSpace = (singleWidth - arcRadius * 2) / 2; //單個導航項減去圓弧直徑後單邊剩餘寬度
Path path = Path() //路徑
..relativeLineTo(moveTween * singleWidth, 0)
..relativeCubicTo(restSpace + padding, 0, restSpace + padding / 2,
arcRadius, singleWidth / 2, arcRadius) //圓弧左半邊
..relativeCubicTo(arcRadius, 0, arcRadius - padding, -arcRadius,
restSpace + arcRadius, -arcRadius) //圓弧右半邊
..relativeLineTo(width - (moveTween + 1) * singleWidth, 0)
..relativeLineTo(0, height)
..relativeLineTo(-width, 0)
..relativeLineTo(0, -height)
..close();
paint.style = PaintingStyle.fill;
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
複製代碼
先將整個導航欄背景的外框繪製出來,再填充成白色,就能獲得咱們想要的帶圓弧形缺口的形狀。Flutter 中的繪製方法有兩種(並不徹底是這樣,有的方法只有一種),拿 relativeLineTo
來講,與其對應的另外一個方法是 lineTo
。二者的區別在於,relativeLineTo
在繪製結束後,會將結束點做爲新的座標系原點(0,0),而 lineTo
的原點始終在左上角(這個說法不嚴謹,兩個方法的原點都是左上角,這裏的意思是,它不會移動)。我這裏使用的 relative*
方法就是由於不用繪製一筆後還要考慮下一筆開始的位置,比較方便,我很喜歡。
這裏最複雜(對我來講)的就是圓弧部分的繪製了,用到了三次貝塞爾曲線(本身手工在草稿紙上畫了一下每一個點的位置,沒辦法,就是這麼菜),須要注意的是,在繪製完圓弧左半邊後,原點移動到了圓弧最底部,所以繪製右半邊圓弧的座標與左半邊是相反的,剩下的就直接畫就行。
最後一步,實現 _FloatNavigatorState
中的動畫控制方法 _switchNav
:
//切換導航
_switchNav(int newIndex) {
double oldPosition = _activeIndex.toDouble();
double newPosition = newIndex.toDouble();
if (oldPosition != newPosition &&
_animationController.status != AnimationStatus.forward) {
_animationController.reset();
_moveAnimation = Tween(begin: oldPosition, end: newPosition).animate(
CurvedAnimation(
parent: _animationController, curve: Curves.easeInCubic))
..addListener(() {
setState(() {
_moveTween = _moveAnimation.value;
});
})
..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
setState(() {
_activeIndex = newIndex;
});
}
});
_animationController.forward();
}
}
複製代碼
這裏每次點擊切換導航的時候都從新給 _moveAnimation
的 begin
和 end
賦值,來肯定要移動的真正距離,當動畫執行完成後,更新當前激活項。
還有一點,差點漏了,銷燬動畫控制器:
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
複製代碼
至此,代碼就寫完了,看一下動態效果:
感受導航項少一些彷佛更好看,完整代碼請點這裏
只能說大致上實現了這個效果,但仍是有一些不足:
這些不足仍是會讓最終效果不那麼完美,但現已足夠。你們有什麼好的想法或建議能夠交流,暢所欲言。
錄製了一套 Flutter 實戰教程,有興趣的能夠看一下