【Flutter 元件集錄】Card | 8 月更文挑戰

張風捷特烈發表於2021-08-20
前言:

這是我參與8月更文挑戰的第 20 天,活動詳情檢視:8月更文挑戰。為應掘金的八月更文挑戰,我準備在本月挑選 31 個以前沒有介紹過的元件,進行全面分析和屬性介紹。這些文章將來會作為 Flutter 元件集錄 的重要素材。希望可以堅持下去,你的支援將是我最大的動力~

本系列元件文章列表
1.NotificationListener2.Dismissible3.Switch
4.Scrollbar5.ClipPath6.CupertinoActivityIndicator
7.Opacity8.FadeTransition9. AnimatedOpacity
10. FadeInImage11. Offstage12. TickerMode
13. Visibility14. Padding15. AnimatedContainer
16.CircleAvatar17.PhysicalShape18.Divider
19.Flexible、Expanded 和 Spacer 20.Card [本文]

一、 認識 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.noneClip.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 方法可以提供繪製操作,這裡簡單在區域左上角畫個小圈。預設 borderOnForegroundtrue,繪製的裝飾會顯在前景中,如下圖。

@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 元件來替代 ContainerImage 原始碼中是怎麼說的:

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 時多看看內部的實現方式,就可以將很多元件聯絡到一塊,很多曾經的疑惑點,也就能迎刃而解。瞭解了內部的實現,在使用時,也會多幾分底氣。那本文到這裡就結束了,謝謝觀看,明天見~

相關文章