使用 flutter 繪製 複雜 tabs 導航

前言

最近在使用 flutter 編寫 app 時遇到一個很使人頭疼的設計稿,具體效果以下:css

image

image

image

能夠發現,圖形是不規則的,同時這種不規則的圖形在不一樣的狀況下展現效果也不同,若是使用圖片解決,又會出現陰影不協調的問題,因此得用到裁剪屬性。html

ClipPath

在flutter中實現這種不規則的圖形,須要用到 ClipPath 這個 widget,具體用法以下:git

class HeaderLeftClipPath extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    const itemWidth = 168.0;
    const bottomHeight = 0;
    var path = Path()
      ..moveTo(0, size.height)
      ..lineTo(0, 20)
      ..quadraticBezierTo(0, 0, 20, 0)
      ..lineTo(itemWidth - 40, 0)
      ..quadraticBezierTo(itemWidth - 20, 0, itemWidth - 20, 20)
      ..lineTo(itemWidth - 20, size.height - 20 - bottomHeight)
      ..quadraticBezierTo(itemWidth - 20, size.height - bottomHeight, itemWidth, size.height - bottomHeight)
      ..lineTo(size.width, size.height - bottomHeight)
      ..lineTo(size.width, size.height)
      ..close();

    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

class Header extends StatelessWidget {
    @override
    build() {
        return _buildLeftHeader()
    }

    _buildLeftHeader() {
        return ClipPath(
            clipper: HeaderLeftClipPath(),
            child: Container(
                width: 168,
                height: 60,
                padding: EdgeInsets.only(right: 20),
                decoration: BoxDecoration(
                    color: Color(0xffff0000)
                ),
            ),
        );
    }
}
複製代碼

它的做用在於根據定義的路徑進行裁剪後獲得須要的圖形,其中繪製路徑時可使用 flutter 提供的 api 進行特殊路徑的繪製,例如貝塞爾曲線,通過上述裁剪,就能獲得這樣一個圖形,也就是訂單頭部的左側導航。github

image

接着咱們再繪製中間部位的導航形狀。api

class HeaderCenterClipPath extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    var path = Path()
      ..moveTo(0, size.height)
      ..quadraticBezierTo(20, size.height, 20, size.height -  20)
      ..lineTo(20, 20)
      ..quadraticBezierTo(20, 0, 40, 0)
      ..lineTo(size.width - 40, 0)
      ..quadraticBezierTo(size.width - 20, 0, size.width - 20, 20)
      ..lineTo(size.width - 20, size.height - 20)
      ..quadraticBezierTo(size.width - 20, size.height, size.width, size.height)
      ..close();

    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

class Header extends StatelessWidget {
    @override
    build() {
        return _buildCenterHeader()
    }

    _buildCenterHeader() {
        return ClipPath(
            clipper: HeaderCenterClipPath(),
            child: Container(
                width: 187,
                height: 60,
                padding: EdgeInsets.only(right: 20),
                decoration: BoxDecoration(
                    color: Color(0xffff0000)
                ),
            ),
        );
    }
}
複製代碼

獲得形狀以下:app

image

最後再繪製右側導航less

class HeaderRightClipPath extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    print(size);
    var path = Path()
      ..moveTo(0, size.height)
      ..quadraticBezierTo(20, size.height, 20, size.height - 20)
      ..lineTo(20, 20)
      ..quadraticBezierTo(20, 0, 40, 0)
      ..lineTo(size.width - 20, 0)
      ..quadraticBezierTo(size.width, 0, size.width, 20)
      ..lineTo(size.width, size.height)
      ..close();

    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

class Header extends StatelessWidget {
    @override
    build() {
        return _buildRightHeader()
    }

    _buildRightHeader() {
        return ClipPath(
            clipper: HeaderRightClipPath(),
            child: Container(
                width: 187,
                height: 60,
                padding: EdgeInsets.only(right: 20),
                decoration: BoxDecoration(
                    color: Color(0xffff0000)
                ),
            ),
        );
    }
}
複製代碼

獲得最終的圖案ide

image

將他們使用 Stack 佈局彙總在一塊兒,獲得效果以下。佈局

image

有點醜,哈哈,主要的緣由是缺乏陰影,以及背景色與設計稿不符,因此如今咱們給剪切後的圖形添加陰影效果。須要注意的是,若是直接給 ClipPath 部件包裹 Container,而且添加陰影效果,是達不到設計稿那樣的效果的,緣由在於即便 Container 被裁剪,但實際的大小仍是原來的大小,因此陰影部分也須要繪製來達到效果。ui

繪製曲線陰影

由於自身也是 flutter 的新手,對於曲線陰影這種效果也不知道如何實現,因而在 google 中搜索獲得瞭解決方案,具體看這裏

話很少說,直接 command cv。

使用此組件,對上面的代碼進行改造,獲得最終效果以下:

image

再把背景色切換爲白色:

image

效果更加明顯,爲了使頭部與底部融合爲一體,須要在視覺上對用戶進行欺騙,因此得把底部的陰影去掉。

class HeaderContainerPath extends CustomClipper<Rect> {
  @override
  Rect getClip(Size size) {
    // TODO: implement getClip
    return Rect.fromLTRB(-10, 0, size.width, size.height);
  }

  @override
  bool shouldReclip(CustomClipper<Rect> oldClipper) {
    // TODO: implement shouldReclip
    return false;
  }
}

_buildHeader() {
    return ClipRect(
      clipper: HeaderContainerPath(),
      child: Container(
        height: 60,
        child: Stack(
          children: <Widget>[
            Positioned(
              left: 0,
              child: _buildLeftHeader(),
            ),
            Positioned(
              left: 135 - 8.0,
              child: _buildCenterHeader(),
            ),
            Positioned(
              left: 283 - 9.0,
              child: _buildRightHeader(),
            )
          ],
        ),
      ),
    );
  }
複製代碼

由於頭部的裁剪是一個矩形,因此咱們這裏須要用到 ClipRect 這個部件,同時 CustomClipper 範型須要指定爲 Rect, 同時 getClip 返回一個 Rect 對象。由於須要保留最左側和頭部的陰影,因此裁剪時,須要向左和向上偏移 10px。

Rect.fromLTRB(-10, -10, size.width, size.height)

獲得效果以下:

image

再給底部容器添加陰影,獲得一個融合爲一體的容器,如圖:

image

效果貌似還不錯,不過有一點細節沒有完成,那就是激活的 tab 會有一個陰影效果覆蓋其它的 tab,如圖所示:

image

若是用常規的思惟來實現,那麼很是麻煩,這裏咱們換一個思惟方式來實現這個效果,添加一個漸變容器來模擬陰影效果。代碼以下所示:

_buildHeader() {
    return ClipRect(
        clipper: HeaderContainerPath(),
        child: Container(
            height: 60,
            child: Stack(
                children: <Widget>[
                    Positioned(
                        left: 0,
                        child: _buildLeftHeader(),
                    ),
                    Positioned(
                        left: 135 - 8.0,
                        child: _buildCenterHeader(),
                    ),
                    Positioned(
                        bottom: 0,
                        left: 0,
                        right: 0,
                        child: _buildShadow(), // 陰影放置在倒數第二的位置
                    ),
                    Positioned(
                        left: 283 - 9.0,
                        child: _buildRightHeader(),
                    )
                ],
            ),
        ),
    );
}

Widget _buildShadow() {
    return Container(
        height: 8,
        decoration: BoxDecoration(
        gradient: LinearGradient(colors: [Color.fromRGBO(255, 255,255, 0), Color.fromRGBO(0, 0, 0, 0.1)], begin: Alignment.topCenter, end: Alignment.bottomCenter)
        ),
    );
}


複製代碼

結果如圖:

image

右邊的陰影有點深,是由於疊加了兩層陰影,這個以後再解決。陰影容器放在倒數第二的位置是由於 flutter 沒有 css 中 zIndex 的概念,層級是以代碼的順序爲準,在 stack 佈局中,寫在最後的代碼層級是最高的,因此陰影放置在倒數第二的位置,覆蓋其它的 tab, 同時保證當前激活的 tab 不會被覆蓋。

點擊進行切換

接下來進行點擊切換的講解,由於 flutter 不存在 zIndex,因此在點擊的時候,咱們須要改變 widget 在代碼中的位置來提高激活 tab 的層級,代碼以下:

_buildHeader() {
    List<Function> tabOrder = [_buildLeftHeader, _buildCenterHeader, _buildRightHeader];
    Function activeOrder = tabOrder.removeAt(activeIndex); // 先移除並取出激活的 tab
    tabOrder = tabOrder.reversed.toList();
    tabOrder.add(_buildShadow); // 把陰影放到倒數第二的位置
    tabOrder.add(activeOrder); // 最終將激活的 tab 放入最後

    return ClipRect(
      clipper: HeaderContainerPath(),
      child: Container(
        height: 60,
        child: Stack(
          children: tabOrder.map<Widget>((fn) => fn()).toList(),
        ),
      ),
    );
  }
複製代碼

最終效果以下:

QQ20190911-160722

最後奉上倉庫地址:

github.com/Richard-Cho…

相關文章
相關標籤/搜索