Flutter 繪制實踐 | 路徑篇 - 陰影模糊

歡迎來到 Flutter 繪制實踐系列饲嗽,本文有文章版和視頻版野芒。視頻發(fā)布在 bilibli 同名賬號下扳剿,文章首發(fā)于掘金平臺。今天來的話題是: 繪制陰影

Flutter 繪制實踐系列視頻鏈接:


1.陰影的繪制

說起 Flutter 繪制陰影掂恕,很多朋友可能都知道 Canvas 本身有 drawShadow 方法〕诨保可以根據(jù)入?yún)⒌?Path 路徑竹海,繪制陰影。就像我們男人分不清口紅色號一樣丐黄,覺得口紅就是紅色斋配,很多編程者也會覺得陰影就是影子。如果一個方法就能繪制出完美的陰影灌闺,也不會單獨寫篇文章艰争。


Canvas 中提供的 drawShadow 方法是根據(jù) 影深 elevation進行繪制陰影的。如下所示桂对,是 drawShadow 繪制圓角矩形路徑的效果甩卓,其中第四入是 bool 值,用于控制路徑內部是否填充:

transparentOccluder:true transparentOccluder:false
 @override
 void paint(Canvas canvas, Size size) {
   canvas.translate(size.width / 2, size.height / 2);
   Path path = Path()
     ..addRRect(
       RRect.fromRectAndRadius(Rect.fromPoints(
         Offset.zero,
         const Offset(100,60),
       ), const Radius.circular(8)),
     );
   canvas.drawShadow(path, Colors.blue.withOpacity(0.2), 5, false);
 }

一般來說不會只是單繪制一個路徑陰影蕉斜,還要繪制影子的發(fā)出者逾柿。如下所示,在陰影上方繪制白色的 "實物"宅此。這樣陰影的效果就有了机错,并且隨著 elevation 的增加,陰影會越深:

void drawBox(Canvas canvas,double elevation){
  Path path = Path()
    ..addRRect(
      RRect.fromRectAndRadius(Rect.fromPoints(
        Offset.zero,
        const Offset(100,60),
      ), const Radius.circular(8)),
    );
  canvas.drawShadow(path, Colors.blue.withOpacity(0.2), elevation, false);
  Paint whitePaint =  Paint()..color=Colors.white;
  canvas.drawPath(path,whitePaint);
}

其實可以發(fā)現(xiàn) drawShadow 只能根據(jù)影深來控制陰影效果父腕,無法調節(jié)陰影偏移量弱匪、陰影半徑、陰影擴散等屬性璧亮。所以萧诫,我們需要尋找到一種途徑斥难,來繪制更加復雜的路徑陰影。比如 css 中的陰影樣式定義:


2. BoxDecoration 中陰影的使用

在日常開發(fā)中帘饶,我們或多或少使用過裝飾屬性哑诊,比如 ContainerDecoratedBox 組件可以通過BoxDecoration 確定裝飾的效果。其中就包含對陰影的處理及刻,而且和 css 中的陰影樣式是一致的:

css 中 box-shadow 是一個列表搭儒,每組由五個參數(shù),分別表示:

x偏移量 | y偏移量 | 陰影模糊半徑 | 陰影擴散半徑 | 陰影顏色

而 Flutter 中的 BoxDecoration#boxShadow 也是一個列表提茁,其中配置參數(shù)由 BoxShadow 類記錄淹禾。參數(shù)的作用和 css 中的是一致的,offset 表示偏移量茴扁,blurRadius 表示陰影模糊半徑铃岔,spreadRadius 表示陰影擴散半徑。


這樣如果已知 css 中的陰影樣式峭火,就很容易將其在 Flutter 中展示出來毁习。如下是 Element UI 的全局彈框的陰影效果,在 Flutter 中的表現(xiàn):源碼詳見 box_decoration

// ElementUI 陰影
BoxDecoration element = BoxDecoration(
  color: Colors.white,
  borderRadius: BorderRadius.circular(8),
  border: Border.all(color:const Color(0xffebeef5)),
  boxShadow: [
    BoxShadow(
      color: Colors.black.withOpacity(0.1),
      offset: const Offset(0,2),
      blurRadius: 12,
      spreadRadius: 0,
    )
  ],
);

如下是 Ant Design 全局彈框的陰影效果卖丸,其中的 box-shadow 由三個陰影確定:

對于 Flutter 而言纺且,就是在 boxShadow 中提供多個 BoxShadow 對象:

// Ant Design 陰影
BoxDecoration decoration = BoxDecoration(
  color: Colors.white,
  borderRadius: BorderRadius.circular(8),
  boxShadow: [
    BoxShadow(
      color: Colors.black.withOpacity(0.08),
      offset: const Offset(0,6),
      blurRadius: 16,
      spreadRadius: 0,
    ),
    BoxShadow(
      color: Colors.black.withOpacity(0.12),
      offset: const Offset(0,3),
      blurRadius: 6,
      spreadRadius: -4,
    ),
    BoxShadow(
      color: Colors.black.withOpacity(0.05),
      offset: const Offset(0,9),
      blurRadius: 28,
      spreadRadius: 8,
    )
  ],
);

3. BoxDecoration 是如何繪制的

既然 BoxDecoration 可以靈活地處理陰影樣式,那么問題來了稍浆,在 Canvas 的繪制中载碌,如何使用呢?其實仔細思考一下衅枫,F(xiàn)lutter 中的一切組件都是繪制出來的嫁艇,那么 BoxDecoration 自然也不例外。那么只要查閱源碼弦撩,看一下它的繪制邏輯步咪,自然可以知道這種陰影是如何實現(xiàn)的。那接下來開始探秘吧益楼。

首先來看 DecoratedBox 組件對應的渲染對象的繪制方法折柠。如下所示飒货,BoxDecoration 裝飾對象通過 createBoxPainter 方法創(chuàng)建繪制對象:

也就是說 BoxDecoration 只承載繪制的信息赌蔑,繪制的邏輯由繪制對象承擔拌蜘。 通過源碼可以看出創(chuàng)建的繪制對象是 _BoxDecorationPainter, 也就是說該類中可能會是繪制陰影邏輯發(fā)生的場所:


果然不出所料,繪制陰影的核心邏輯就在這個類中俊扭,會遍歷 boxShadow 列表繪制陰影區(qū)域队橙。其中偏移量 offset 是矩形區(qū)域的移動;擴散半徑 spreadRadius 是矩形區(qū)域的擴大萨惑;最后剩下一個模糊長度 blurRadius 還未知:

在上面 447 行中捐康,畫筆是通過 boxShadow 對象會取的,進入其中可以發(fā)現(xiàn)畫筆會設置一個模糊的蒙版庸蔼。蒙版的模糊參數(shù)由 blurRadius 確定解总。到這里我們就清楚了,BoxDecoration 實現(xiàn)陰影的本質是通過 模糊蒙版姐仅。所以我們也可以借鑒這種思路花枫,來處理繪制時的路徑陰影。

double get blurSigma => convertRadiusToSigma(blurRadius);

4. 通過 BoxShadow 繪制陰影

BoxDecoration 本身用于矩形類的陰影繪制掏膏,像路徑這種不規(guī)則的圖形不能直接使用劳翰。所以需要進行一些處理,特別是 spreadRadius 對陰影的擴散處理馒疹。如下是封裝的方法佳簸,路徑的偏移很簡單,可以通過 shift 進行處理颖变。難點在于 spreadRadius 的處理生均,這里對 path 沿矩形區(qū)域中心進行縮放來實現(xiàn)擴散的效果。關于 Path 的簡單變換腥刹,在上一篇已經(jīng)介紹過了马胧。

void drawShadows(Canvas canvas, Path path, List<BoxShadow> shadows) {
  for (final BoxShadow shadow in shadows) {
    final Paint shadowPainter = shadow.toPaint();
    if (shadow.spreadRadius == 0) {
      canvas.drawPath(path.shift(shadow.offset), shadowPainter);
    } else {
      Rect zone = path.getBounds();
      double xScale = (zone.width + shadow.spreadRadius) / zone.width;
      double yScale = (zone.height + shadow.spreadRadius) / zone.height;
      Matrix4 m4 = Matrix4.identity();
      m4.translate(zone.width / 2, zone.height / 2);
      m4.scale(xScale, yScale);
      m4.translate(-zone.width / 2, -zone.height / 2);
      canvas.drawPath(path.shift(shadow.offset).transform(m4.storage), shadowPainter);
    }
  }
  Paint whitePaint = Paint()..color = Colors.white;
  canvas.drawPath(path, whitePaint);
}

這樣,只要指定路徑和 BoxShadow 列表即可繪制出路徑的陰影衔峰。

// 第一個 
drawShadows(canvas, rect, [
  BoxShadow(
    color: Colors.black.withOpacity(0.1),
    offset: const Offset(0, 2),
    blurRadius: 6,
    spreadRadius: 0,
  )
]);

// 第二個 
drawShadows(canvas, rect, [
  BoxShadow(
    color: Colors.redAccent.withOpacity(0.2),
    offset: const Offset(0, 2),
    blurRadius: 8,
    spreadRadius: 0,
  )
]);

// 第三個 
drawShadows(canvas, rect, [
  BoxShadow(
    color: Colors.black.withOpacity(0.08),
    offset: const Offset(0,6),
    blurRadius: 16,
    spreadRadius: 0,
  ),
  BoxShadow(
    color: Colors.black.withOpacity(0.12),
    offset: const Offset(0,3),
    blurRadius: 6,
    spreadRadius: -4,
  ),
  BoxShadow(
    color: Colors.black.withOpacity(0.05),
    offset: const Offset(0,9),
    blurRadius: 28,
    spreadRadius: 8,
  )
]);

這樣佩脊,不止是矩形區(qū)域,對于任何路徑來說垫卤,都可以實現(xiàn)陰影效果邻吞。這種陰影可以設置偏移、模糊半徑葫男、擴散半徑抱冷,也可以設置多陰影,如下的第二片雪花是兩個陰影的疊加梢褐。另外旺遮,雪花路徑的繪制在之前視頻中介紹過,這里就不貼代碼了盈咳,詳見源碼 shadow_painter_v2耿眉。

drawShadows(
    canvas,
    path,
    [BoxShadow(
      color: Colors.blue.withOpacity(0.2),
      offset: const Offset(0, 2),
      blurRadius: 6,
      spreadRadius: 0,
    ),
      BoxShadow(
        color: Colors.redAccent.withOpacity(0.2),
        offset: const Offset(0, -2),
        blurRadius: 6,
        spreadRadius: 0,
      )
    ]);

本文,我們探索了 Flutter 繪制中鱼响,如何用 Canvas 繪制復雜的陰影樣式鸣剪。期間通過 BoxDecoration 在源碼中的繪制邏輯,發(fā)現(xiàn)其本質是通過模糊遮罩來實現(xiàn)陰影效果。并借此思路筐骇,對 Path 的陰影繪制進行加強债鸡,使其可以根據(jù) BoxShadow 列表繪制陰影。那本文就到這里铛纬,以后還會帶來更多關于 Flutter 繪制的知識厌均,歡迎多多關注和支持,下次再見 ~

作者:張風捷特烈
鏈接:https://juejin.cn/post/7194250504518500409

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末告唆,一起剝皮案震驚了整個濱河市棺弊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌擒悬,老刑警劉巖模她,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異懂牧,居然都是意外死亡侈净,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門归苍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來用狱,“玉大人,你說我怎么就攤上這事拼弃∠囊粒” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵吻氧,是天一觀的道長溺忧。 經(jīng)常有香客問我,道長盯孙,這世上最難降的妖魔是什么鲁森? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮振惰,結果婚禮上歌溉,老公的妹妹穿的比我還像新娘。我一直安慰自己骑晶,他們只是感情好痛垛,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著桶蛔,像睡著了一般匙头。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仔雷,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天蹂析,我揣著相機與錄音舔示,去河邊找鬼。 笑死电抚,一個胖子當著我的面吹牛惕稻,可吹牛的內容都是我干的。 我是一名探鬼主播喻频,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼缩宜,長吁一口氣:“原來是場噩夢啊……” “哼肘迎!你這毒婦竟也來了甥温?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤妓布,失蹤者是張志新(化名)和其女友劉穎姻蚓,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匣沼,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡狰挡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了释涛。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片加叁。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖唇撬,靈堂內的尸體忽然破棺而出它匕,到底是詐尸還是另有隱情,我是刑警寧澤窖认,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布豫柬,位于F島的核電站,受9級特大地震影響扑浸,放射性物質發(fā)生泄漏烧给。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一喝噪、第九天 我趴在偏房一處隱蔽的房頂上張望础嫡。 院中可真熱鬧,春花似錦酝惧、人聲如沸榴鼎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽檬贰。三九已至,卻和暖如春缺亮,著一層夾襖步出監(jiān)牢的瞬間翁涤,已是汗流浹背桥言。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留葵礼,地道東北人号阿。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像鸳粉,于是被迫代替她去往敵國和親扔涧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容