網上已經有不少文章展示如何使用 CircularNotchedRectangle
或 AutomaticNotchedShape
實現帶凹陷效果的 BottomAppBar,但是都沒有提到如何自定義 NotchedShape 實現任意形狀的 BottomAppBar,本文以閒魚底部導航為例展示如何自定義 NotchedShape。
首先假設你已經知道如何使用 CircularNotchedRectangle
建立凹陷效果,我們主要做的就是寫一個新的 class 繼承 NotchedShape
來實現我們想要的效果。
在開始寫 NotchedShape
之前我們先準備好一個正常的 BottomAppBar,因為不是重點,具體的實現就不展開說了,看起來像是這樣的:
唯一值得一提的是,中間的按鈕是一個正常的 FloatingActionButton
,為了讓它比原來大並能超出 BottomAppBar 而使用了 Transform.scale
。
接下來,我們建立新的 class CustomNotchedShape
繼承 NotchedShape
並 override getOuterPath
:
class CustomNotchedShape extends NotchedShape {
final BuildContext context;
const CustomNotchedShape(this.context);
@override
Path getOuterPath(Rect host, Rect? guest) {
return Path();
}
}
複製程式碼
NotchedShape
和 CustomClipper<Path>
很像,都是通過建立一個 path 來定義我們想要的形狀,如果你有 CustomClipper<Path>
的經驗,那 NotchedShape
也就差不多了。唯一不一樣的是 Path getOuterPath(Rect host, Rect? guest)
有兩個引數,host
在這裡就是 BottomAppBar 本身的矩形邊界,guest
則是嵌入 BottomAppBar 的矩形邊界。由於我們並不需要嵌入 guest,第二個引數可以忽略掉。
我們可以先簡單寫一個梯形 path 來驗證。
Path getOuterPath(Rect host, Rect? guest) {
return Path()
..moveTo(host.left + 20, host.top)
..lineTo(host.right - 20, host.top)
..lineTo(host.right, host.bottom)
..lineTo(host.left, host.bottom);
}
複製程式碼
效果:
知道怎麼使用 path 建立自定義形狀之後就可以思考如何實現閒魚底部導航的效果了,經過觀察,這樣的效果可以由圓弧或貝塞爾曲線組合而成,使用貝塞爾曲線可能是最簡單的,而且只需要一個控制點的貝塞爾曲線。
我們可以先畫輪廓直線,然後再換成單個控制點的貝塞爾曲線即可。
Path getOuterPath(Rect host, Rect? guest) {
const radius = 40.0;
const lx = 20.0;
const ly = 8;
var x = (MediaQuery.of(context).size.width - radius) / 2 - lx;
return Path()
..moveTo(host.left, host.top)
..lineTo(x, host.top)
..lineTo(x += lx, host.top - ly)
..lineTo(x += radius, host.top - ly)
..lineTo(x += lx, host.top)
..lineTo(host.right, host.top)
..lineTo(host.right, host.bottom)
..lineTo(host.left, host.bottom);
}
複製程式碼
lx 和 ly 是旁邊兩條斜線的 x、y,radius 是頂部橫線的長度。
然後我們替換成貝塞爾曲線以及選擇合適的控制點,再引入兩個變數 bx、by 作為控制點的 x、y 偏移量,所有引數調整到合適到值之後即可實現最終效果:
Path getOuterPath(Rect host, Rect? guest) {
const radius = 40.0;
const lx = 20.0;
const ly = 8;
const bx = 10.0;
const by = 20.0;
var x = (MediaQuery.of(context).size.width - radius) / 2 - lx;
return Path()
..moveTo(host.left, host.top)
..lineTo(x, host.top)
..quadraticBezierTo(x + bx, host.top, x += lx, host.top - ly)
..quadraticBezierTo(
x + radius / 2, host.top - by, x += radius, host.top - ly)
..quadraticBezierTo((x += lx) - bx, host.top, x, host.top)
..lineTo(host.right, host.top)
..lineTo(host.right, host.bottom)
..lineTo(host.left, host.bottom);
}
複製程式碼
DartPad 線上演示:dartpad.dev/?null_safet…