前言:
這是我參與8月更文挑戰的第 20 天,活動詳情檢視:8月更文挑戰。為應掘金的八月更文挑戰
,我準備在本月挑選 31
個以前沒有介紹過的元件,進行全面分析和屬性介紹。這些文章將來會作為 Flutter 元件集錄
的重要素材。希望可以堅持下去,你的支援將是我最大的動力~
一、 認識 Card 元件
卡片效果作為 Material Design
中的一員,Flutter 中 Card
元件自然是要有的。原始碼註釋中是這麼描述它的:帶有輕微圓角和立面陰影的皮膚。
本文將從原始碼的角度看一下 Card
元件的構成,並講述一下 Card
在使用中的一些細小的注意點。
1.Card 基本資訊
下面是 Card
元件類的定義
和 構造方法
,可以看出它繼承自 StatelessWidget
。沒有必須要傳入的引數,可以配置顏色、陰影色、形狀、邊距等屬性。
2.Card 的簡單使用
如下所示,通過 buildContent
返回 Container
元件作為內容。上層用 Card
元件的包裹後,會有小圓角
+ 陰影
的效果,其中 color
屬性就是皮膚的顏色。
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Card(
color: const Color(0xffB3FE65),
child: buildContent(),
),
),
);
}
Widget buildContent() {
return Container(
width: 200,
height: 0.618 * 200,
padding: const EdgeInsets.all(10),
child: Text("Card : 卡片", style: TextStyle(fontSize: 20)));
}
}
複製程式碼
2. shadowColor 和 elevation 屬性
通過 shadowColor
可以設定陰影的顏色,通過 elevation
可以設定陰影的深度。
Card(
color: Color(0xffB3FE65),
shadowColor: Colors.blueAccent,
elevation: 8,
child: buildContent(),
)
複製程式碼
3.margin 屬性
單獨一個 Card
也許看不清外邊距,可以使用兩個輔助的 box 看一下。如下,可以看出 Card
預設是有外邊距的。調節外邊距的屬性便是 margin
。
使用下面的程式碼,就可以讓左外邊距為 20,右外邊距為 30.
Card(
margin: EdgeInsets.only(left: 20,right: 30),
color: Color(0xffB3FE65),
child: buildContent(),
)
複製程式碼
4. clipBehavior 裁剪行為
Clip
是一個列舉類,包含四種形式,如下:
enum Clip {
none, // 無
hardEdge, // 硬邊緣
antiAlias, // 抗鋸齒
antiAliasWithSaveLayer, // 抗鋸齒儲存圖層
}
複製程式碼
如下左圖,在內容的容器中使用圖片裝飾,你會很疑惑,為什麼沒有圓角了。因為 Card
的預設裁剪行為為 Clip.none
。這時需要通過指定 clipBehavior
完成圓角,這是一個小細節,不知道的話很可能覺得 Card 元件不好用。
Clip.none | Clip.antiAlias |
---|---|
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child:
Card(
clipBehavior: Clip.antiAlias, //<--- 裁剪行為
color: const Color(0xffB3FE65),
child: buildContent(),
),
),
);
}
Widget buildContent() {
return Container(
width: 200,
height: 0.618 * 200,
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration( //<--- 新增裝飾圖片
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage('assets/images/anim_draw.webp')
)
),
child: Text("Card : 卡片", style: TextStyle(fontSize: 20,color: Colors.white)));
}
}
複製程式碼
5. shape 屬性
前面只是簡單的屬性配置,而你 Card
的強大不止於此。也許你會覺得預設的圓角有點小,想要變大點,或不喜歡圓角裝飾,先要搞點創造性裝飾,那麼 shape
屬性將為你開啟一扇大門。需要的是一個 ShapeBorder
物件,由於其為抽象類,需要找它的子類,框架中提供如下的子類。關於 shape 屬性的適應,之前在《Path在手,天下我有》 中詳細介紹過,這裡不再贅述。
比如想要增加圓角,可以使用 RoundedRectangleBorder
形狀。
Card(
clipBehavior: Clip.antiAlias,
color: const Color(0xffB3FE65),
shape: const RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 3,
shadowColor: Colors.blueAccent,
child: buildContent(),
),
複製程式碼
除了內建的形狀之外,我們還可以自己定義 Shape
, 比如下面通過 nStarPath
獲取一個多角星的路徑,然後在繼承自 ShapeBorder 的 StarShapeBorder#getOuterPath
中返回路徑,就可以按照該路徑進行裁剪。這裡為了方便,多角星的資料寫死了,外界的容器寬高該為 100
。
class StarShapeBorder extends ShapeBorder {
@override
EdgeInsetsGeometry get dimensions => null;
@override
Path getInnerPath(Rect rect, {TextDirection textDirection}) {
return null;
}
@override
Path getOuterPath(Rect rect, {TextDirection textDirection}) =>
nStarPath(9, 50, 40, dx: 50, dy: 50);
@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
}
@override
ShapeBorder scale(double t) {
return null;
}
Path nStarPath(int num, double R, double r, {dx = 0, dy = 0}) {
Path _path = Path();
_path.reset(); //重置路徑
double perRad = 2 * pi / num; //每份的角度
double radA = perRad / 2 / 2; //a角
double radB = 2 * pi / (num - 1) / 2 - radA / 2 + radA; //起始b角
_path.moveTo(cos(radA) * R + dx, -sin(radA) * R + dy); //移動到起點
for (int i = 0; i < num; i++) { //迴圈生成點,路徑連至
_path.lineTo(
cos(radA + perRad * i) * R + dx, -sin(radA + perRad * i) * R + dy);
_path.lineTo(
cos(radB + perRad * i) * r + dx, -sin(radB + perRad * i) * r + dy);
}
_path.close();
return _path;
}
}
複製程式碼
5.borderOnForeground 屬性
這個屬性估計沒人在意它,它可以決定 ShapeBorder
的繪製是否顯示在前景之中。通過上面可以看到 StarShapeBorder
中有個 paint
方法可以提供繪製操作,這裡簡單在區域左上角畫個小圈。預設 borderOnForeground
為 true
,繪製的裝飾會顯在前景中,如下圖。
@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
canvas.drawCircle(Offset.zero, 50, Paint()..color=Colors.blueAccent);
}
複製程式碼
如果 borderOnForeground
設定為 false,就說明繪製的內容不出現在前景中。
二、Card 的水波紋
1.錯誤的使用
如果你將 InkWell
放在了 Center
之上,那麼它水波紋會被前景所覆蓋。如下圖所示,之上 margin
的那點區域顯示出來水波紋。
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
onTap: (){
},
child:Card(
clipBehavior: Clip.antiAlias,
color: const Color(0xffB3FE65),
elevation: 3,
shadowColor: Colors.blueAccent,
child: buildContent()),
),
),
);
}
複製程式碼
2. 正確的使用
正確的使用方式是在 child 元件上巢狀 InkWell
。
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Card(
clipBehavior: Clip.antiAlias,
color: const Color(0xffB3FE65),
elevation: 3,
shadowColor: Colors.blueAccent,
child: InkWell(
splashColor: Colors.blue.withAlpha(30),
onTap: (){
},
child:buildContent()),
),
),
);
}
複製程式碼
3.無法觸發水波紋的解決方案
有些時候,比如使用 Image
、或為 Container
設定顏色、裝之後,水波紋就無法觸發。
這是可以通過 Ink
元件來替代 Container
或 Image
原始碼中是怎麼說的:
Widget buildContent() {
return Ink(
width: 200,
height: 0.618 * 200,
decoration: const BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage('assets/images/anim_draw.webp'))),
child: Padding(
padding: const EdgeInsets.all(20),
child: Text("Card: 卡片",
style: TextStyle(fontSize: 20,color: Colors.white)),
));
}
複製程式碼
三、Card 的原始碼分析
好啦,又到最後看原始碼的時間了。Card
元件作為一個 StatelessWidget
,肯定是 “白嫖”
了別的元件功能。核心程式碼如下:可以看出它就是一個 Container
+ Material
元件的組合體。
那為什麼一個件簡單的的物件,要單獨抽離一個 Card
元件呢?很明顯,語義明確,簡單易用,簡單
就是王道。另外一點就是可以同一設定 CardTheme
來決定 Card
的預設表現。如果沒有 Card
元件,想達到效果可以用 Material
元件,但每次用都要設定很多物件,而且無法設定主題,使用封裝是為了更好地使用。
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final CardTheme cardTheme = CardTheme.of(context);
return Semantics(
container: semanticContainer,
child: Container(
margin: margin ?? cardTheme.margin ?? const EdgeInsets.all(4.0),
child: Material(
type: MaterialType.card,
shadowColor: shadowColor ?? cardTheme.shadowColor ?? theme.shadowColor,
color: color ?? cardTheme.color ?? theme.cardColor,
elevation: elevation ?? cardTheme.elevation ?? _defaultElevation,
shape: shape ?? cardTheme.shape ?? const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
borderOnForeground: borderOnForeground,
clipBehavior: clipBehavior ?? cardTheme.clipBehavior ?? Clip.none,
child: Semantics(
explicitChildNodes: !semanticContainer,
child: child,
),
),
),
);
}
複製程式碼
通過之前看的幾個 StatelessWidget
的元件可以發現,這種型別的元件主要的目的就是方便使用者使用
,其內部都是依賴於別的元件實現的,使用在看 StatelessWidget
時多看看內部的實現方式,就可以將很多元件聯絡到一塊,很多曾經的疑惑點,也就能迎刃而解。瞭解了內部的實現,在使用時,也會多幾分底氣。那本文到這裡就結束了,謝謝觀看,明天見~