前言:
这是我参与8月更文挑战的第 20 天,活动详情查看:8月更文挑战。为应掘金的八月更文挑战
,我准备在本月挑选 31
个以前没有介绍过的组件,进行全面分析和属性介绍。这些文章将来会作为 Flutter 组件集录
的重要素材。希望可以坚持下去,你的支持将是我最大的动力~
一、 认识 Card 组件
卡片效果作为 Material Design
中的一员,Flutter 中 Card
组件自然是要有的。源码注释中是这么描述它的:带有轻微圆角和立面阴影的面板。
本文将从源码的角度看一下 Card
组件的构成,并讲述一下 Card
在使用中的一些细小的注意点。
1.Card 基本信息
下面是 Card
组件类的定义
和 构造方法
,可以看出它继承自 StatelessWidget
。没有必须要传入的参数,可以配置颜色、阴影色、形状、边距等属性。
2.Card 的简单使用
如下所示,通过 buildContent
返回 Container
组件作为内容。上层用 Card
组件的包裹后,会有小圆角
+ 阴影
的效果,其中 color
属性就是面板的颜色。
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Card(
color: const Color(0xffB3FE65),
child: buildContent(),
),
),
);
}
Widget buildContent() {
return Container(
width: 200,
height: 0.618 * 200,
padding: const EdgeInsets.all(10),
child: Text("Card : 卡片", style: TextStyle(fontSize: 20)));
}
}
2. shadowColor 和 elevation 属性
通过 shadowColor
可以设置阴影的颜色,通过 elevation
可以设置阴影的深度。
Card(
color: Color(0xffB3FE65),
shadowColor: Colors.blueAccent,
elevation: 8,
child: buildContent(),
)
3.margin 属性
单独一个 Card
也许看不清外边距,可以使用两个辅助的 box 看一下。如下,可以看出 Card
默认是有外边距的。调节外边距的属性便是 margin
。
使用下面的代码,就可以让左外边距为 20,右外边距为 30.
Card(
margin: EdgeInsets.only(left: 20,right: 30),
color: Color(0xffB3FE65),
child: buildContent(),
)
4. clipBehavior 裁剪行为
Clip
是一个枚举类,包含四种形式,如下:
enum Clip {
none, // 无
hardEdge, // 硬边缘
antiAlias, // 抗锯齿
antiAliasWithSaveLayer, // 抗锯齿保存图层
}
如下左图,在内容的容器中使用图片装饰,你会很疑惑,为什么没有圆角了。因为 Card
的默认裁剪行为为 Clip.none
。这时需要通过指定 clipBehavior
完成圆角,这是一个小细节,不知道的话很可能觉得 Card 组件不好用。
Clip.none | Clip.antiAlias |
---|---|
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child:
Card(
clipBehavior: Clip.antiAlias, //<--- 裁剪行为
color: const Color(0xffB3FE65),
child: buildContent(),
),
),
);
}
Widget buildContent() {
return Container(
width: 200,
height: 0.618 * 200,
padding: const EdgeInsets.all(10),
decoration: const BoxDecoration( //<--- 添加装饰图片
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage('assets/images/anim_draw.webp')
)
),
child: Text("Card : 卡片", style: TextStyle(fontSize: 20,color: Colors.white)));
}
}
5. shape 属性
前面只是简单的属性配置,而你 Card
的强大不止于此。也许你会觉得默认的圆角有点小,想要变大点,或不喜欢圆角装饰,先要搞点创造性装饰,那么 shape
属性将为你打开一扇大门。需要的是一个 ShapeBorder
对象,由于其为抽象类,需要找它的子类,框架中提供如下的子类。关于 shape 属性的适应,之前在《Path在手,天下我有》 中详细介绍过,这里不再赘述。
比如想要增加圆角,可以使用 RoundedRectangleBorder
形状。
Card(
clipBehavior: Clip.antiAlias,
color: const Color(0xffB3FE65),
shape: const RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 3,
shadowColor: Colors.blueAccent,
child: buildContent(),
),
除了内置的形状之外,我们还可以自己定义 Shape
, 比如下面通过 nStarPath
获取一个多角星的路径,然后在继承自 ShapeBorder 的 StarShapeBorder#getOuterPath
中返回路径,就可以按照该路径进行裁剪。这里为了方便,多角星的数据写死了,外界的容器宽高该为 100
。
class StarShapeBorder extends ShapeBorder {
@override
EdgeInsetsGeometry get dimensions => null;
@override
Path getInnerPath(Rect rect, {TextDirection textDirection}) {
return null;
}
@override
Path getOuterPath(Rect rect, {TextDirection textDirection}) =>
nStarPath(9, 50, 40, dx: 50, dy: 50);
@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
}
@override
ShapeBorder scale(double t) {
return null;
}
Path nStarPath(int num, double R, double r, {dx = 0, dy = 0}) {
Path _path = Path();
_path.reset(); //重置路径
double perRad = 2 * pi / num; //每份的角度
double radA = perRad / 2 / 2; //a角
double radB = 2 * pi / (num - 1) / 2 - radA / 2 + radA; //起始b角
_path.moveTo(cos(radA) * R + dx, -sin(radA) * R + dy); //移动到起点
for (int i = 0; i < num; i++) { //循环生成点,路径连至
_path.lineTo(
cos(radA + perRad * i) * R + dx, -sin(radA + perRad * i) * R + dy);
_path.lineTo(
cos(radB + perRad * i) * r + dx, -sin(radB + perRad * i) * r + dy);
}
_path.close();
return _path;
}
}
5.borderOnForeground 属性
这个属性估计没人在意它,它可以决定 ShapeBorder
的绘制是否显示在前景之中。通过上面可以看到 StarShapeBorder
中有个 paint
方法可以提供绘制操作,这里简单在区域左上角画个小圈。默认 borderOnForeground
为 true
,绘制的装饰会显在前景中,如下图。
@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
canvas.drawCircle(Offset.zero, 50, Paint()..color=Colors.blueAccent);
}
如果 borderOnForeground
设置为 false,就说明绘制的内容不出现在前景中。
二、Card 的水波纹
1.错误的使用
如果你将 InkWell
放在了 Center
之上,那么它水波纹会被前景所覆盖。如下图所示,之上 margin
的那点区域显示出来水波纹。
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
onTap: (){
},
child:Card(
clipBehavior: Clip.antiAlias,
color: const Color(0xffB3FE65),
elevation: 3,
shadowColor: Colors.blueAccent,
child: buildContent()),
),
),
);
}
2. 正确的使用
正确的使用方式是在 child 组件上嵌套 InkWell
。
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Card(
clipBehavior: Clip.antiAlias,
color: const Color(0xffB3FE65),
elevation: 3,
shadowColor: Colors.blueAccent,
child: InkWell(
splashColor: Colors.blue.withAlpha(30),
onTap: (){
},
child:buildContent()),
),
),
);
}
3.无法触发水波纹的解决方案
有些时候,比如使用 Image
、或为 Container
设置颜色、装之后,水波纹就无法触发。
这是可以通过 Ink
组件来替代 Container
或 Image
源码中是怎么说的:
Widget buildContent() {
return Ink(
width: 200,
height: 0.618 * 200,
decoration: const BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage('assets/images/anim_draw.webp'))),
child: Padding(
padding: const EdgeInsets.all(20),
child: Text("Card: 卡片",
style: TextStyle(fontSize: 20,color: Colors.white)),
));
}
三、Card 的源码分析
好啦,又到最后看源码的时间了。Card
组件作为一个 StatelessWidget
,肯定是 “白嫖”
了别的组件功能。核心代码如下:可以看出它就是一个 Container
+ Material
组件的组合体。
那为什么一个件简单的的对象,要单独抽离一个 Card
组件呢?很明显,语义明确,简单易用,简单
就是王道。另外一点就是可以统一设置 CardTheme
来决定 Card
的默认表现。如果没有 Card
组件,想达到效果可以用 Material
组件,但每次用都要设置很多对象,而且无法设置主题,使用封装是为了更好地使用。
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final CardTheme cardTheme = CardTheme.of(context);
return Semantics(
container: semanticContainer,
child: Container(
margin: margin ?? cardTheme.margin ?? const EdgeInsets.all(4.0),
child: Material(
type: MaterialType.card,
shadowColor: shadowColor ?? cardTheme.shadowColor ?? theme.shadowColor,
color: color ?? cardTheme.color ?? theme.cardColor,
elevation: elevation ?? cardTheme.elevation ?? _defaultElevation,
shape: shape ?? cardTheme.shape ?? const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
borderOnForeground: borderOnForeground,
clipBehavior: clipBehavior ?? cardTheme.clipBehavior ?? Clip.none,
child: Semantics(
explicitChildNodes: !semanticContainer,
child: child,
),
),
),
);
}
通过之前看的几个 StatelessWidget
的组件可以发现,这种类型的组件主要的目的就是方便用户使用
,其内部都是依赖于别的组件实现的,使用在看 StatelessWidget
时多看看内部的实现方式,就可以将很多组件联系到一块,很多曾经的疑惑点,也就能迎刃而解。了解了内部的实现,在使用时,也会多几分底气。那本文到这里就结束了,谢谢观看,明天见~