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

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

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


一、認識 Opacity 元件

Opacity 元件定義在框架中的 widgets/basic.dart 中,說明它是一個比較基礎的元件。其作用是:將一個元件 以指定透明度 opacity 進行透明處理


1. Opacity 基本資訊

下面是 Opacity 元件類的定義構造方法,可以看出它繼承自 SingleChildRenderObjectWidget。例項化時必須傳入 opacity 入參,還可以傳入一個 chuild 元件。

Opacity 元件功能很單一,用起來也非常簡單。另外,將 widget 透明還有其他的手段,比如設定顏色的 alpha 值。那使用Opacity 元件有什麼優劣,又有什麼必要性呢,何時該用,何時不該用呢?本文將從原始碼的角度去重新審視 Opacity 元件,讓你認識到一個更全面的 Opacity


2.Opacity 的使用

Opacity 元件的用法非常簡單,只要指定 opacity 即可,下面是 0.1 ~ 0.8 間隔 0.1 的透明度效果。

class OpacityTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 10,
      runSpacing: 20,
      children: [0.1,0.2,0.3, 0.4, 0.5,0.6, 0.7, 0.8]
          .map((opacity) => Opacity(
                opacity: opacity,
                child: Image.asset(
                  'assets/images/icon_head.webp',
                  width: 80,
                  height: 80,
                ),
              ))
          .toList(),
    );
  }
}
複製程式碼

3.基於顏色的透明度

對於 圖片的透明度 ,我們可以使用 color + colorBlendMode 實現,效果和 Opacity 是一樣的。

class OpacityTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 10,
      runSpacing: 20,
      children: [0.1,0.2,0.3, 0.4, 0.5,0.6, 0.7, 0.8]
          .map((opacity) =>
              Image.asset(
                  'assets/images/icon_head.webp',
                  width: 80,
                  height: 80,
                  color: Color.fromRGBO(255, 255, 255, opacity),
                  colorBlendMode: BlendMode.modulate
                ),
              )
          .toList(),
    );
  }
}
複製程式碼

那這兩種方式有什麼差異呢?在此之前,先看一下 Opacity 的原始碼是如何實現的。


二、 Opacity 的原始碼實現

1. Opacity 元件屬性

Opacity 最主要的屬性是 double 型的 opacity ,可以傳入一個 child 元件。通過斷言可以看出 opacity 不可為空,且取值範圍在 [0.0~1.0] 之間。


2.Opacity 維護的 RenderObject

Opacity 繼承自 SingleChildRenderObjectWidget ,作為 RenderObjectWidget 一族,有著建立和維護 RenderObject 的使命。

其中Opacity#createRenderObject 返回的是 RenderOpacity ,也就是說透明化功能的具體實現,是由 RenderOpacity 類物件完成的。Opacity 最為元件,只是起到一個 配置作用


3.認識 RenderOpacity

RenderOpacityRenderObject 的後代,也就是渲染物件,它承擔著佈局繪製的任務。在構造方法中時會根據傳入的 opacity_alpha 成員進行初始化。

其中getAlphaFromOpacityColor 類的靜態方法,作用就是將 0 ~ 1opacity 轉換為 0 ~ 255alpha 值。

---->[Color#getAlphaFromOpacity]----
static int getAlphaFromOpacity(double opacity) {
  assert(opacity != null); // ignore: unnecessary_null_comparison
  return (opacity.clamp(0.0, 1.0) * 255).round();
}
複製程式碼

4.RenderOpacity#paint 方法

RenderOpacity 最重要的是繪製的處理,如下是 RenderOpacity#paint 的邏輯。可以看出,當 child 非空時,如果 _alpha = 0 就什麼都不需要畫,直接返回。如果 _alpha = 255 ,則直接繪製 child

從這裡可以瞭解到,通過 Opacity 元件,設定為 0 透明,可以既保留 child 元件的佔位,又可以避免繪製 child ,是一種非常好的隱藏元件的方式。如果不需要佔位,通過 if 是更好的選擇,如果需要佔位 Opacity 會略勝一籌。比較 用 if 需要在 else 給佔位元件。

最後,如果有透明度時,會通過 context.pushOpacity 進行處理。那 pushOpacity 方法做了什麼呢?


三、認識 PaintingContext#pushOpacity

1.斷點除錯

下面通過一個最精簡的 demo 來除錯看看執行時的情況。

void main() => runApp(OpacityTest());

class OpacityTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Opacity(
        opacity: 0.3,
        child: Image.asset(
        'assets/images/icon_head.webp',
        width: 80,
        height: 80,
    ));
  }
}
複製程式碼

斷點資訊如下,左側棧幀顯示繪製一幀到RenderOpacity#paint 時,方法的入棧情況。RenderObject 有一個 ContainerLayer 型的 _layer 屬性,預設為 null。下一步將執行 context.pushOpacity 。這裡的 context 物件型別為 PaintingContext,入參的 _alpha = 77oldLayer = null


PaintingContext#pushOpacity 入棧後,如果 oldLayer 為空,就會建立 OpacityLayer ,併為其設定 alphaoffset,然後執行 pushLayer 方法。


childLayer 有孩子的話,會移除其孩子,這裡剛建立的 OpacityLayer ,沒有孩子,不會走入 if 程式碼塊。

然後通過 appendlayer 將過程建立的 OpacityLayer 新增到 _containerLayer 中。


2.層的合成

drawFrame 中的 flushLayoutflushCompositingBitsflushPaint 完成後,此時螢幕並不會出現內容。還需要通過 renderView.compositeFrame() 將資料傳送給 GPU

之前的方法的處理完後,元素樹渲染樹都已經形成。作為渲染樹最頂層的節點,renderView 物件會通過 compositeFrame 來合成幀。RenderObject 物件中有 Layer 成員,如下 renderViewlayerTransformLayer ,它也有 childparent 屬性,形成樹狀結構。

上圖的層中只有兩個節點,TransformLayer和它的孩子 OpacityLayer。然後執行 layer!.buildScene(builder) ,建立 ui.Scene 物件。

其中 addToScene 方法會讓孩子執行 addToScene,這樣所有的 Layer 就新增到了 Scene 中。


3. ui.Scene 的用途

通過 renderView 中持有的 TransformLayer 呼叫 buildScene 方法,就可以將其下所以的 Layer 合併到一個 Scene 中,並返回 ui.Scene 物件,最後通過 _window.render 方法進行渲染 ui.Scene 物件。

渲染的方法是一個平臺的 native 方法。總的來看,使用 Opacity 元件,最終是通過一個 OpacityLayer 層實現的透明功能。


四、再看 Opacity 的優劣

Opacity 元件功能實現的方式是直接用 OpacityLayer 完成的,這樣在合成幀的時候就會多一個層,會有些許的昂貴,但是存在就有存在的價值 。使用顏色進行透明是有侷限性的,有些透明度必須要 Opacity 元件。 如果是 ImageColor 需要透明,那麼殺雞焉用牛刀,顏色透明足以。

但當整個 item 或是整個頁面需要透明化,使用 Opacity 元件就會很方便和簡潔。不然你需要對所有關於顏色的地方進行透明度處理。這樣處理會讓程式碼很複雜,可讀性差,而且這樣的操作可能會讓一些物件無法保持 const 常量,比如一個 const Text("XXX"),為了讓其顏色透明,需要使用 Texstyle 定義顏色。而這個透明度需要執行時獲取,那麼這個 Text 無法保持 const,還多出樣式處理的程式碼。

Layer 的層級結構設計出來也是為了使用的,多一個 Layer,也就是在合成時多一些方法的出入棧,並沒有想象中的那麼重。所以 Opacity 該用的時候還是要用的,但對於單一的圖片、顏色的透明處理,能省則省,就不需要用了。

Widget buildItem() {
  return Container(
      height: 80,
      margin: const EdgeInsets.all(10),
      alignment: Alignment.center,
      decoration:  BoxDecoration(
        border: Border.all(color: Colors.red),
        borderRadius: BorderRadius.circular(10)
      ),
      child:  Row(
            children: [
              const SizedBox(width: 15),
              Image.asset(
                'assets/images/icon_head.webp',
                width: 60,
                height: 60,
              ),
              const SizedBox(width: 15,),
              Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text( '張風捷特烈',
                    style:  TextStyle(fontSize: 16),
                  ),
                  const Text( '程式設計之王',
                    style:  TextStyle(fontSize: 14,color: Colors.grey),
                  ),
                ],
              ),
              const Spacer(),
              const Icon(Icons.ac_unit_outlined, color: Colors.blue , size: 24),
              const SizedBox(width: 5,),
              const Icon(Icons.alt_route_rounded, color:  Colors.blue , size: 24,),
              const SizedBox(width: 20,),
            ],
          ),
    );
}
複製程式碼

有人也許會靈光一現,不是有個 ColorFiltered 嗎,指定 ColorFilter 透明不就行了嗎。當你瞄一眼 ColorFiltered 的原始碼,就會發現,它的原理也是通過新增一個 ColorFilterLayer 實現的,所以這樣反而會弄巧成拙。

當你瞭解了這些,對 Opacity 這個元件就會有全面的認識,好與不好,只是使用的場景的優劣,工具本無罪。在使用時留意一下即可,問一下,“我是否可以很方便的通過顏色的透明度實現透明效果”。能則不用 Opacity ,反則用之。另外不要忘記 Opacity 對於單一元件顯隱的優勢。那本文就到這裡,謝謝觀看 ~


Opacity 元件 的使用方式到這裡就介紹完畢,雖然是個簡單的小元件,但通過其原始碼實現,我們還是能學到一點東西的。那本文到這裡就結束了,謝謝觀看,明天見~

相關文章