Flutter完全自定義底部導航

倒敘手法,先上效果圖:


自定義底部導航

最初的想法是使用系統(tǒng)自帶的BottomNavigationBar來實現(xiàn),做到一半發(fā)現(xiàn)完全無法滿足這奇葩合理的需求:

  • 背景模糊效果
  • 凸起按鈕
  • 非標準高度
  • indicator 動畫

起初也曾想到過想要封裝一個相對通用的組件,然而需求太過非標,不可避免地需要重復造車輪子,只能將其中一些思路整理出來。

框架結(jié)構(gòu)

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        alignment: AlignmentDirectional.bottomStart,
        children: [
                    // 頁面
          Container(
            child: PageView(),
          ),
                    // 底部導航
          LATabBar(),
          // indicator動畫
          Transform.translate(),
        ],
      ),
    );
  }

我們使用Stack 來放置子視圖,簡單粗暴,從底部開始層疊:

  • Container:子頁面,使用PageView 進行頁面切換
  • LATabBar:自定義底部導航
  • Transform.translate():indicator動畫組件

出于盡可能解耦的原因,這里把indicator 放到底部導外。

接下來,我們將重點關(guān)注底部導航的構(gòu)建。

全局常量

定義全局常量 ,以針對不同設備進行適配。

Flutter定義全局常量

底部導航

考慮到這里有一個模糊效果,使用Stack來層疊backgrounditem ,首先來定義相關(guān)屬性。

class LATabBar extends StatefulWidget {
    final double height;
  final Color backgroundColor;
  final Widget background;
  final List<LATabItem> items;

    ...
}
  • height:底部導航視圖高度
  • backgroundColor:背景顏色(考慮后續(xù)組件化設計)
  • background:自定義背景組件(模糊+凸起效果)
  • items:自定義NavigationItem

自定義背景

先前說過,此方法很難封裝成一個通用的組件。因為根據(jù)需求,需要自定義的東西太多,標準組件所需默認值也多。因此,只能將其中的思路記錄下來。

繪制不規(guī)則圖形

在iOS開發(fā)中,此類不規(guī)則背景,只需使用一個UIImageView 加載帶透明度圖片,隨后使用view.mask 即可實現(xiàn)不規(guī)則背景。

而在Flutter 中,并無相應API來渲染(我太蔡了),因此,需要使用到自定義裁切ClipPath 來繪制相應不規(guī)則圖形。

首先使用貝塞爾曲線,自定義一個clipper:

class RaisedClipper extends CustomClipper<Path> {

  @override
  Path getClip(Size size) {
    /**凸起高度*/
    double raisedHeight = 18;
    /**凸起寬度*/
    double raisedWidth = 80;
    /**凸起右邊距*/
    double paddingTrailing = 25;

    
    Path path = Path();
    path.lineTo(0, raisedHeight);
    double cubicStartX = size.width - paddingTrailing - raisedWidth - 10;
    // 貝塞爾曲線起始位置
    path.lineTo(cubicStartX, raisedHeight);
    // 繪制曲線
    path.cubicTo(
        cubicStartX + raisedWidth * 0.1, raisedHeight,
        cubicStartX + raisedWidth * 0.25, 0,
        cubicStartX + raisedWidth * 0.5, 0);
    path.cubicTo(
        cubicStartX + raisedWidth * 0.75, 0,
        cubicStartX + raisedWidth * 0.85, raisedHeight,
        size.width - paddingTrailing, raisedHeight);

    path.lineTo(size.width, raisedHeight);
    path.lineTo(size.width, size.height);
    path.lineTo(0, size.height);

    return path;
  }

關(guān)于貝塞爾曲線,這里不再贅述,分享一個在線預覽工具,方便調(diào)試。

模糊效果

Flutter提供了BackdropFilter 來作為高斯模糊的組件,配合使用ImageFilter 來制作模糊效果。

class BlurBackground extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: RaisedClipper(),
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
        child: Container(
          color: Colors.white.withOpacity(0.5),
        ),
      ),
    );
  }
}

這樣就完成了自定義底部導航背景視圖的編碼。

導航切換按鈕

先上代碼:

enum LATabItemStyle {
  normal,
  titleOnly,
  iconOnly,
}

class LATabItem extends StatefulWidget {
  final double width;
  final String iconNormal;
  final String iconSelected;
  final EdgeInsets iconInsets;
  final Alignment iconAlignment;
  final Color titleNormalColor;
  final Color titleSelectedColor;
  final EdgeInsets titleInsets;
  final Alignment titleAlignment;
  final String title;
  final GestureTapCallback onTap;
  final bool selected;
  final LATabItemStyle style;

  const LATabItem({
    Key key,
    this.width,
    this.iconNormal,
    this.iconSelected,
    this.iconInsets = EdgeInsets.zero,
    this.iconAlignment = Alignment.center,
    this.titleNormalColor,
    this.titleSelectedColor,
    this.titleInsets = EdgeInsets.zero,
    this.titleAlignment = Alignment.center,
    this.title,
    this.onTap,
    this.selected = false,
    this.style = LATabItemStyle.normal}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return LATabItemState();
  }
}

class LATabItemState extends State<LATabItem> {
  @override
  Widget build(BuildContext context) {
    Widget content;
    switch (this.widget.style) {
      case LATabItemStyle.titleOnly:

        break;

      case LATabItemStyle.iconOnly:
        content = Container(
          width: this.widget.width,
          alignment: this.widget.iconAlignment,
          padding: this.widget.iconInsets,
          child: Image.asset(this.widget.selected ? this.widget.iconSelected : this.widget.iconNormal),
        );
        break;

      default:

        break;
    }

    if (this.widget.width == null || this.widget.width <= 0) {
      return Expanded(
        child: GestureDetector(
          onTap: this.widget.onTap,
          child: content,
          behavior: HitTestBehavior.opaque,
        ),
      );
    } else {
      return GestureDetector(
        onTap: this.widget.onTap,
        child: content,
      );
    }
  }
}

導航按鈕完全可以使用自定義的widget 而不必拘泥于形式,可以是自定義的文字、圖片、繪制的圖形各種。只要將其作為GestureDetectorchild 即可響應點擊事件。

需要注意的是,如果使用自適應大小的Expanded ,則需要指定GestureDetectorbehavior: HitTestBehavior.opaque

indicator動畫

簡單動畫,直接使用AnimationControllerTween 來制作動畫。

  • 初始化:
_animationController = AnimationController(duration: Duration(milliseconds: 100), vsync: this);
_tween = Tween(begin: leading, end: leading);
_animation = _tween.animate(_animationController)
  ..addListener(() {
    setState(() {});
  });
  • Translation容器:
Transform.translate(
        offset: Offset(_animation.value, 0),
        child: Container(
        margin: EdgeInsets.fromLTRB(0, 0, 0, Global.tabBarHeight + Global.paddingBottom - Global.tabBarRaisedHeight - 4),
        width: 20,
        height: 4,
        decoration: BoxDecoration(
        borderRadius: BorderRadius.all(Radius.circular(2)),
        color: Color(0xFFB60005),
     ),
   ),
 ),
  • 切換視圖時,執(zhí)行動畫:
void setIndicator(int index) {
    _tween.begin = _tween.end;
    _animationController.reset();
    _tween.end = unitWidth * index + leading;
    _animationController.forward();
}

到這里,自定義底部導航完成。

最后放上demo。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容