Flutter 元件 | 手牽手,一起走 CompositedTransformFollower 與 CompositedTransformTarget

張風捷特烈發表於2021-04-02

一、緣起

CompositedTransformTarget 合成變換目標
CompositedTransformFollower 合成變換伴隨者
複製程式碼

這兩個元件已加入 FlutterUnit, 可更新檢視,順便 star ~。

CompositedTransformTargetCompositedTransformFollower


  1. Slider 元件開始說起

其實之前這兩個元件我一直都不知道它們是幹嘛用的,直到有一天我在看 Slider 的原始碼時發現了他倆。我們都知道,當 Slider 元件設定了 labeldivisions 時,在拖動的過程中會彈出 Overlay 提示框。

當我們使用 TransformSlider 進行旋轉變換,可以發現Overlay 浮層也進行了相應的旋轉變換。如果在不知道 CompositedTransformTargetCompositedTransformFollower 元件之前,也許你會以為這個變換是在 Slider 原始碼中算出來作用在 Overlay 浮層上,其實不然。

Transform(
  transform: Matrix4.rotationZ(-15/180*pi),
  alignment: Alignment.center,
  child: Slider(
    ...
  ),
),
複製程式碼

2.兩者在 slider 原始碼中的使用

CompositedTransformFollower 是伴隨者,可以看成跟屁蟲。在 Slider 元件中 Overlay 對應的元件的外層包裹了 CompositedTransformFollower,表示其身份是一個伴隨者。

Slider 核心構建元件 _SliderRenderObjectWidget 的外層包裹了 CompositedTransformTarget。標誌著其為被跟隨的目標。兩者通過 _layerLink 訂立連線的契約,從而達到 變換與共 的效果。

用這兩個元件有什麼好處呢?其實很明顯。在不知道這兩個元件之前,我們是如何確定 Overlay 的位置呢?一個字:。通過 Positioned 元件和元件位置資訊得到確切位置,對於一些靜態的Overlay 框,也許可行。但是如果伴隨滑動旋轉縮放時,那必須通過計算來更新 Overlay 的位置,這顯然是非常麻煩的。


二、自己試驗一下

Slider 作為框架的原始碼元件,是比較複雜的,不是所有人都有耐心一點點分享。為了方便演示這兩者的使用,我特意準備了幾個精簡的演示案例。


1.最簡使用

現在要實現如下效果:點選時,在元件的左上角顯示一個 Overlay 提示資訊,再點選則隱藏。如果先不看下面的實現,你可能會想到使用 Positioned 元件,通過RenderBox算出元件的左上角的 絕對座標來放置 Overlay。這樣算起來是比較麻煩的,而且有些人估計也不知道怎麼算。


整體邏輯很簡單,測試demo中目標元件是Image,上層包裹了 CompositedTransformTarget 。在點選時,執行 _toggleOverlay 方法來切換 Overlay 的顯隱情況。

class TipBox extends StatefulWidget {
  TipBox({Key key}) : super(key: key);
  @override
  _TipBoxState createState() => _TipBoxState();
}

class _TipBoxState extends State<TipBox> {
  final LayerLink layerLink = LayerLink();
  OverlayEntry _overlayEntry;
  bool show = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _toggleOverlay,
      child: CompositedTransformTarget(
        link: layerLink,
        child: Image.asset(
          "assets/images/icon_head.webp",
          width: 80, height: 80,
        ),
      ),
    );
  }

  void _toggleOverlay() {
    if (!show) {
      _showOverlay();
    } else {
      _hideOverlay();
    }
    show = !show;
  }

  void _showOverlay() {
    _overlayEntry = _createOverlayEntry();
    Overlay.of(context).insert(_overlayEntry);
  }

  void _hideOverlay() {
    _overlayEntry?.remove();
  }
}
複製程式碼

伴隨元件是浮窗內容,上層包裹了 CompositedTransformFollower 。由於預設清空下 OverlayEntry 的約束條件會強行撐滿全屏,可以使用 UnconstrainedBox 來解除約束。通過兩個元件的伴隨,實現了在不通過計算的情況下,使 Overlay 停留在目標元件的左上角。

OverlayEntry _createOverlayEntry() => OverlayEntry(
    builder: (BuildContext context) => UnconstrainedBox(
        child: CompositedTransformFollower(
          link: layerLink,
          child: Material(
            child: Container(
              alignment: Alignment.center,
                decoration: BoxDecoration(
                    color: Colors.blue,
                    borderRadius: BorderRadius.circular(5)),
                padding: const EdgeInsets.all(10),
                width: 50,
                child: const Text("toly",style: TextStyle(color: Colors.white),)),
          ),
        ),
      ),
  );
複製程式碼

2.伴隨者的相對位置

可能有人會問,可以控制 Overlay 停留的位置嗎,實現偏移或者對齊。對於伴隨者 可以設定 offset(偏移)targetAnchor(目標錨點)followerAnchor(伴隨者錨點) 來控制停留的位置。

比如下面,通過設定對齊方式偏移可以實現:Overlay 置於對應元件的左側。

followerAnchor: Alignment.centerLeft,
targetAnchor: Alignment.centerRight,
offset: Offset(5,0),
複製程式碼

也可以放置在對應元件上方,其實有了對齊方式,你想放在哪都行。由於是相對位置,就省去了很多不必要的計算。

followerAnchor: Alignment.bottomCenter,
targetAnchor: Alignment.topCenter,
offset: Offset(0,-5),
複製程式碼

結合我的 Wrapper 元件(wrapper: ^1.0.1),這樣就可以很輕鬆實現點選彈出浮框的效果。

OverlayEntry _createOverlayEntry() => OverlayEntry(
    builder: (BuildContext context) => UnconstrainedBox(
      child: CompositedTransformFollower(
            link: layerLink,
            followerAnchor: Alignment.bottomCenter,
            targetAnchor: Alignment.topCenter,
            offset: Offset(0,-5),
            child: Material(
              child: Container(
                width: 150,
                child: Wrapper(
                  color: Colors.white,
                  spineType: SpineType.bottom,
                  elevation: 1,
                    offset:70,
                  shadowColor: Colors.grey.withAlpha(88),
                  child: Text("張風捷特烈 " * 5),
                ),
              ),
            ),
          ),
    ));
複製程式碼

通過設定對其方位,很容易控制位置。如果通過傳統的 Positioned 元件,想換個位置,還需要一通計算。用著兩個哥們,就非常方便。


像這種圖示點選的 Overlay 提示欄,使用著兩個哥們就很好定位。


3.變換的一致性

如果說有槓精說:不用這倆,我就喜歡算,怎麼啦。 但變換一致性的儲存是之前很難辦到的。這裡我將滑動也是為一種變換,滑動本質上也是一種平移變換

  • 旋轉:

Transform(
  transform: Matrix4.rotationZ(-15/180*pi),
  alignment: Alignment.center,
  child: TipBox(),
),
複製程式碼

  • 縮放:

Transform(
  transform: Matrix4.diagonal3Values(0.5,0.5,1),
  alignment: Alignment.center,
  child: TipBox(),
),
複製程式碼

  • 斜切:

Transform(
  transform: Matrix4.skewX(15/180*pi),
  alignment: Alignment.center,
  child: TipBox(),
),
複製程式碼

  • 平移:

Transform(
  transform: Matrix4.translationValues(150,0,0),
  alignment: Alignment.center,
  child: TipBox(),
),
複製程式碼

很明顯,CompositedTransformFollower 會伴隨 CompositedTransformTarget 進行變換,這樣無論在滑動、縮放、旋轉等操作中,兩者都會保持相對的繫結關係,無需計算,皆大歡喜。


三、CompositedTransformFollower 只能用於 Overlay 嗎

先給個答案,並非。如下,在 Stack 中,兩個普通的元件也可以保持繫結關係。但話說回來,這樣做並沒有什麼意義。普通元件間的對其佈局很完善,FlexWrap 都可以,並不需要這兩個哥們插一腳。而 Overlay 作為一個 孤魂野鬼,需要有個繩把它拴著。

class TipBox extends StatelessWidget{
  final LayerLink layerLink = LayerLink();

  @override
  Widget build(BuildContext context) {
    return  Stack(
        children: [
          CompositedTransformTarget(
            link: layerLink,
            child: Image.asset(
              "assets/images/icon_head.webp",
              width: 50,
              height: 50,
            ),
          ),

          CompositedTransformFollower(
            link: layerLink,
            followerAnchor: Alignment.topLeft,
            targetAnchor: Alignment.topRight,
            offset: Offset(5,0),
            child: Material(
              child: Container(
                width: 150,
                child: Wrapper(
                  color: Color(0xff95EC69),
                  spineType: SpineType.left,
                  elevation: 1,
                  offset:20,
                  shadowColor: Colors.grey.withAlpha(88),
                  child: Text("張風捷特烈 " * 5),
                ),
              ),
            ),
          ),
        ],
    );
  }
}
複製程式碼

那本文就到這裡,謝謝觀看~


@張風捷特烈 2021.04.02 未允禁轉
我的公眾號:程式設計之王
聯絡我--郵箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~

相關文章