最近在使用 flutter 编写 app 时遇到一个很使人头疼的设计稿,具体效果以下:css
能够发现,图形是不规则的,同时这种不规则的图形在不一样的状况下展现效果也不同,若是使用图片解决,又会出现阴影不协调的问题,因此得用到裁剪属性。html
在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
接着咱们再绘制中间部位的导航形状。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
最后再绘制右侧导航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
将他们使用 Stack 布局汇总在一块儿,获得效果以下。布局
有点丑,哈哈,主要的缘由是缺乏阴影,以及背景色与设计稿不符,因此如今咱们给剪切后的图形添加阴影效果。须要注意的是,若是直接给 ClipPath 部件包裹 Container,而且添加阴影效果,是达不到设计稿那样的效果的,缘由在于即便 Container 被裁剪,但实际的大小仍是原来的大小,因此阴影部分也须要绘制来达到效果。ui
由于自身也是 flutter 的新手,对于曲线阴影这种效果也不知道如何实现,因而在 google 中搜索获得了解决方案,具体看这里。
话很少说,直接 command cv。
使用此组件,对上面的代码进行改造,获得最终效果以下:
再把背景色切换为白色:
效果更加明显,为了使头部与底部融合为一体,须要在视觉上对用户进行欺骗,因此得把底部的阴影去掉。
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)
获得效果以下:
再给底部容器添加阴影,获得一个融合为一体的容器,如图:
效果貌似还不错,不过有一点细节没有完成,那就是激活的 tab 会有一个阴影效果覆盖其它的 tab,如图所示:
若是用常规的思惟来实现,那么很是麻烦,这里咱们换一个思惟方式来实现这个效果,添加一个渐变容器来模拟阴影效果。代码以下所示:
_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)
),
);
}
复制代码
结果如图:
右边的阴影有点深,是由于叠加了两层阴影,这个以后再解决。阴影容器放在倒数第二的位置是由于 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(),
),
),
);
}
复制代码
最终效果以下:
最后奉上仓库地址: