前言
前段時(shí)間萄焦,學(xué)習(xí)到了Flutter動(dòng)畫不恭,正愁不知道寫個(gè)項(xiàng)目練習(xí)鞏固灵妨,突然有一天產(chǎn)品在群里發(fā)了一個(gè)鏈接【ios中的動(dòng)畫標(biāo)簽】(下面有例圖)衣陶,我心里直呼"好家伙"柄瑰,要是產(chǎn)品都要求做成這樣,產(chǎn)品經(jīng)理和程序員又又又又又又得打起來! 還好只是讓我們參考剪况,剛好可以拿來練習(xí)教沾。
GitHub地址:https://github.com/longer96/flutter-demo
我們每天都會(huì)看到底部導(dǎo)航菜單,它們?cè)趹?yīng)用程序內(nèi)引導(dǎo)用戶译断,允許他們?cè)诓煌膖ag之間快速切換授翻。但是誰說切換標(biāo)簽就應(yīng)該很無聊?
讓我們一起探索標(biāo)簽欄中有趣的動(dòng)畫。雖然你在應(yīng)用程序中可能不會(huì)使用到堪唐,但看看它的實(shí)現(xiàn)可能會(huì)給你提供一些靈感巡语、設(shè)計(jì)參考。
如果恰好能給你帶來一點(diǎn)點(diǎn)幫助淮菠,那是再好不過啦~ 路過的帥逼幫忙點(diǎn)個(gè) star
先上幾張花里胡哨的底部菜單 參考圖
效果分析
咳咳男公,有的動(dòng)效確實(shí)挺難的,需要設(shè)計(jì)師的鼎力支持兜材,我只好選軟的柿子捏
首先我們觀察理澎,它是由文字和指示器組成的。點(diǎn)擊之后指示器切換曙寡,文字縮放。
- 每個(gè)tag 均分了屏幕寬度
- 點(diǎn)擊之后寇荧,指示器從之前的tag中部位置拉長(zhǎng)到選中tag的中部位置
- 指示器到達(dá)選中tag之后举庶,長(zhǎng)度立馬向選中tag位置收縮
稍微復(fù)雜一點(diǎn)的是指示器的動(dòng)畫,看上去有3個(gè)變量:左邊距揩抡、右邊距户侥、指示器寬度。
但變量越多峦嗤,越不方便控制蕊唐,細(xì)心想一下 我們發(fā)現(xiàn)其實(shí)只需要控制: 左、右邊距就可以了烁设,指示器寬度設(shè)置成自適應(yīng)(或者只控制左邊距和指示器寬度)
實(shí)現(xiàn)效果
其實(shí)很多類似底部菜單都可以如法炮制替梨,指示器位于tag后面,根據(jù)不同的條件調(diào)整位置和尺寸装黑。
實(shí)現(xiàn)一款底部菜單
常見的還有另一種展開類似的菜單副瀑,比如這樣
咱們還是先簡(jiǎn)單分析一下
- 由一個(gè)按鈕、多個(gè)tag按鈕組成
- 點(diǎn)擊之后恋谭,tag呈扇狀展開或收縮
看上去只有2步糠睡,還是很簡(jiǎn)單的嘛
第一步:我們用幀布局疊放按鈕和tag
Stack(
children: [
// tag菜單
// 菜單/關(guān)閉 按鈕
]
)
第二步:管理好tag的位置
簡(jiǎn)單介紹一下Flow,F(xiàn)lutter中Flow是一個(gè)對(duì)子組件尺寸以及位置調(diào)整非常高效的控件疚颊。
Flow用轉(zhuǎn)換矩陣在對(duì)子組件進(jìn)行位置調(diào)整的時(shí)候進(jìn)行了優(yōu)化:在Flow定位過后狈孔,如果子組件的尺寸或者位置發(fā)生了變化,在FlowDelegate中的paintChildren()方法中調(diào)用context.paintChild 進(jìn)行重繪材义,而context.paintChild在重繪時(shí)使用了轉(zhuǎn)換矩陣均抽,并沒有實(shí)際調(diào)整組件位置。
使用起來也很簡(jiǎn)單母截,只需要實(shí)現(xiàn)FlowDelegate的paintChildren()方法到忽,就可以自定義布局策略。所以我們需要計(jì)算好每一個(gè)tag的軌跡位置。
經(jīng)過你的細(xì)心觀察喘漏,你發(fā)現(xiàn)tag的軌跡呈半圓狀展開护蝶,對(duì) 沒錯(cuò) 就是需要翻出三角函數(shù)
經(jīng)過你的又一次細(xì)心觀察,你發(fā)現(xiàn)有5個(gè)tag翩迈,半圓實(shí)際可以放7個(gè)持灰,但是為了有更好的顯示效果,可以將需要展示的tag放在中間位置(過濾掉第一個(gè)和最后一個(gè))
所以我們可以列出簡(jiǎn)單的計(jì)算
final total = context.childCount + 1;
for (int i = 0; i < childCount; i++) {
x = cos(pi * (total - i - 1) / total) * Radius;
y = sin(pi * (total - i - 1) / total) * Radius;
}
你發(fā)現(xiàn)太規(guī)整的圓其實(shí)并不是那么好看负饲,優(yōu)化一下
- 將x軸半徑設(shè)置為 父級(jí)約束寬度的一半
- 將Y軸半徑設(shè)置為 父級(jí)約束高度
- 給動(dòng)畫加上曲線堤魁,讓tag有類似回彈效果
-
注意y軸得轉(zhuǎn)換為負(fù)數(shù),因?yàn)槲覀兊淖鴺?biāo)點(diǎn)位于下方
a003.gif
微調(diào)一下返十,好啦 恭喜你!
3句代碼妥泉,讓產(chǎn)品經(jīng)理給你點(diǎn)了18杯茶
class FlowAnimatedCircle extends FlowDelegate {
final Animation<double> animation;
/// icon 尺寸
final double iconSize = 48.0;
/// 菜單左右邊距
final paddingHorizontal = 8.0;
FlowAnimatedCircle(this.animation) : super(repaint: animation);
@override
void paintChildren(FlowPaintingContext context) {
// 進(jìn)度等于0,也就是收起來的時(shí)候不繪制
final progress = animation.value;
if (progress == 0) return;
final xRadius = context.size.width / 2 - paddingHorizontal;
final yRadius = context.size.height - iconSize;
// 開始(0,0)在父組件的中心
double x = 0;
double y = 0;
final total = context.childCount + 1;
for (int i = 0; i < context.childCount; i++) {
x = progress * cos(pi * (total - i - 1) / total) * xRadius;
y = progress * sin(pi * (total - i - 1) / total) * yRadius;
// 使用Matrix定位每個(gè)子組件
context.paintChild(
i,
transform: Matrix4.translationValues(
x, -y + (context.size.height / 2) - (iconSize / 2), 0),
);
}
}
@override
bool shouldRepaint(FlowAnimatedCircle oldDelegate) => false;
}
只要理解到了上面的實(shí)現(xiàn)洞坑,下面這3種也能很輕松完成
最后
收集盲链、參考實(shí)現(xiàn)了幾個(gè)底部導(dǎo)航,當(dāng)然可能很多地方需要優(yōu)化迟杂,大家不要噴我哦
- 有很棒的底部菜單希望推薦
- 需要使用的刽沾,建議大家clone下來,直接引入排拷,具體需求(如未讀消息)自己添加
- 歡迎Fork & pr貢獻(xiàn)您的代碼侧漓,大家共同學(xué)習(xí)?
- Android 體驗(yàn)下載 http://d.cc53.cn/sn6c
- Web在線體驗(yàn) http://footer.eeaarr.cn