最近在使用 flutter 編寫 app 時遇到一個很使人頭疼的設計稿,具體效果以下:css
能夠發現,圖形是不規則的,同時這種不規則的圖形在不一樣的狀況下展現效果也不同,若是使用圖片解決,又會出現陰影不協調的問題,因此得用到裁剪屬性。html
在flutter中實現這種不規則的圖形,須要用到 ClipPath 這個 widget,具體用法以下:git
class HeaderLeftClipPath extends CustomClipper<Path> {
@override
Path getClip(Size size) {
const itemWidth = 168.0;
const bottomHeight = 0;
var path = Path()
..moveTo(0, size.height)
..lineTo(0, 20)
..quadraticBezierTo(0, 0, 20, 0)
..lineTo(itemWidth - 40, 0)
..quadraticBezierTo(itemWidth - 20, 0, itemWidth - 20, 20)
..lineTo(itemWidth - 20, size.height - 20 - bottomHeight)
..quadraticBezierTo(itemWidth - 20, size.height - bottomHeight, itemWidth, size.height - bottomHeight)
..lineTo(size.width, size.height - bottomHeight)
..lineTo(size.width, size.height)
..close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
class Header extends StatelessWidget {
@override
build() {
return _buildLeftHeader()
}
_buildLeftHeader() {
return ClipPath(
clipper: HeaderLeftClipPath(),
child: Container(
width: 168,
height: 60,
padding: EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: Color(0xffff0000)
),
),
);
}
}
複製代碼
它的做用在於根據定義的路徑進行裁剪後獲得須要的圖形,其中繪製路徑時可使用 flutter 提供的 api 進行特殊路徑的繪製,例如貝塞爾曲線,通過上述裁剪,就能獲得這樣一個圖形,也就是訂單頭部的左側導航。github
接着咱們再繪製中間部位的導航形狀。api
class HeaderCenterClipPath extends CustomClipper<Path> {
@override
Path getClip(Size size) {
var path = Path()
..moveTo(0, size.height)
..quadraticBezierTo(20, size.height, 20, size.height - 20)
..lineTo(20, 20)
..quadraticBezierTo(20, 0, 40, 0)
..lineTo(size.width - 40, 0)
..quadraticBezierTo(size.width - 20, 0, size.width - 20, 20)
..lineTo(size.width - 20, size.height - 20)
..quadraticBezierTo(size.width - 20, size.height, size.width, size.height)
..close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
class Header extends StatelessWidget {
@override
build() {
return _buildCenterHeader()
}
_buildCenterHeader() {
return ClipPath(
clipper: HeaderCenterClipPath(),
child: Container(
width: 187,
height: 60,
padding: EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: Color(0xffff0000)
),
),
);
}
}
複製代碼
獲得形狀以下:app
最後再繪製右側導航less
class HeaderRightClipPath extends CustomClipper<Path> {
@override
Path getClip(Size size) {
print(size);
var path = Path()
..moveTo(0, size.height)
..quadraticBezierTo(20, size.height, 20, size.height - 20)
..lineTo(20, 20)
..quadraticBezierTo(20, 0, 40, 0)
..lineTo(size.width - 20, 0)
..quadraticBezierTo(size.width, 0, size.width, 20)
..lineTo(size.width, size.height)
..close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
class Header extends StatelessWidget {
@override
build() {
return _buildRightHeader()
}
_buildRightHeader() {
return ClipPath(
clipper: HeaderRightClipPath(),
child: Container(
width: 187,
height: 60,
padding: EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: Color(0xffff0000)
),
),
);
}
}
複製代碼
獲得最終的圖案ide
將他們使用 Stack 佈局彙總在一塊兒,獲得效果以下。佈局
有點醜,哈哈,主要的緣由是缺乏陰影,以及背景色與設計稿不符,因此如今咱們給剪切後的圖形添加陰影效果。須要注意的是,若是直接給 ClipPath 部件包裹 Container,而且添加陰影效果,是達不到設計稿那樣的效果的,緣由在於即便 Container 被裁剪,但實際的大小仍是原來的大小,因此陰影部分也須要繪製來達到效果。ui
由於自身也是 flutter 的新手,對於曲線陰影這種效果也不知道如何實現,因而在 google 中搜索獲得瞭解決方案,具體看這裏。
話很少說,直接 command cv。
使用此組件,對上面的代碼進行改造,獲得最終效果以下:
再把背景色切換爲白色:
效果更加明顯,爲了使頭部與底部融合爲一體,須要在視覺上對用戶進行欺騙,因此得把底部的陰影去掉。
class HeaderContainerPath extends CustomClipper<Rect> {
@override
Rect getClip(Size size) {
// TODO: implement getClip
return Rect.fromLTRB(-10, 0, size.width, size.height);
}
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) {
// TODO: implement shouldReclip
return false;
}
}
_buildHeader() {
return ClipRect(
clipper: HeaderContainerPath(),
child: Container(
height: 60,
child: Stack(
children: <Widget>[
Positioned(
left: 0,
child: _buildLeftHeader(),
),
Positioned(
left: 135 - 8.0,
child: _buildCenterHeader(),
),
Positioned(
left: 283 - 9.0,
child: _buildRightHeader(),
)
],
),
),
);
}
複製代碼
由於頭部的裁剪是一個矩形,因此咱們這裏須要用到 ClipRect 這個部件,同時 CustomClipper 範型須要指定爲 Rect, 同時 getClip 返回一個 Rect 對象。由於須要保留最左側和頭部的陰影,因此裁剪時,須要向左和向上偏移 10px。
Rect.fromLTRB(-10, -10, size.width, size.height)
獲得效果以下:
再給底部容器添加陰影,獲得一個融合爲一體的容器,如圖:
效果貌似還不錯,不過有一點細節沒有完成,那就是激活的 tab 會有一個陰影效果覆蓋其它的 tab,如圖所示:
若是用常規的思惟來實現,那麼很是麻煩,這裏咱們換一個思惟方式來實現這個效果,添加一個漸變容器來模擬陰影效果。代碼以下所示:
_buildHeader() {
return ClipRect(
clipper: HeaderContainerPath(),
child: Container(
height: 60,
child: Stack(
children: <Widget>[
Positioned(
left: 0,
child: _buildLeftHeader(),
),
Positioned(
left: 135 - 8.0,
child: _buildCenterHeader(),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: _buildShadow(), // 陰影放置在倒數第二的位置
),
Positioned(
left: 283 - 9.0,
child: _buildRightHeader(),
)
],
),
),
);
}
Widget _buildShadow() {
return Container(
height: 8,
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Color.fromRGBO(255, 255,255, 0), Color.fromRGBO(0, 0, 0, 0.1)], begin: Alignment.topCenter, end: Alignment.bottomCenter)
),
);
}
複製代碼
結果如圖:
右邊的陰影有點深,是由於疊加了兩層陰影,這個以後再解決。陰影容器放在倒數第二的位置是由於 flutter 沒有 css 中 zIndex 的概念,層級是以代碼的順序爲準,在 stack 佈局中,寫在最後的代碼層級是最高的,因此陰影放置在倒數第二的位置,覆蓋其它的 tab, 同時保證當前激活的 tab 不會被覆蓋。
接下來進行點擊切換的講解,由於 flutter 不存在 zIndex,因此在點擊的時候,咱們須要改變 widget 在代碼中的位置來提高激活 tab 的層級,代碼以下:
_buildHeader() {
List<Function> tabOrder = [_buildLeftHeader, _buildCenterHeader, _buildRightHeader];
Function activeOrder = tabOrder.removeAt(activeIndex); // 先移除並取出激活的 tab
tabOrder = tabOrder.reversed.toList();
tabOrder.add(_buildShadow); // 把陰影放到倒數第二的位置
tabOrder.add(activeOrder); // 最終將激活的 tab 放入最後
return ClipRect(
clipper: HeaderContainerPath(),
child: Container(
height: 60,
child: Stack(
children: tabOrder.map<Widget>((fn) => fn()).toList(),
),
),
);
}
複製代碼
最終效果以下:
最後奉上倉庫地址: