使用 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…

相关文章
相关标签/搜索