Flutter 122: 图解自定义半遮挡头像 SeriesCi
小菜在学习过程中,想实现一行半遮挡的用户头像的功能,横向展示过程中,具体包括 右侧头像逐个半遮挡左侧头像 和 左侧头像逐个半遮挡右侧头像 两种展示;
- 可展示本地图或网络图;
- 可自定义末尾图标;
- 可自定义边框样式;
整个自定义过程主要是通过基础的 Stack 层级和 Positioned 设置偏移量来完成,小菜仅记录一下在测试过程中遇到的小问题;
SeriesCircleProfile
1. 左右半遮挡
小菜预想的核心功能,是实现不同方向的叠加遮挡效果;其中合适准备采用 Stack 小组件作为头像的层级展示,在通过 Positioned 设置偏移量来设置半遮挡效果;其中 Stack 中的子 Widget 是以栈的方式存储的,即数组后面的元素会覆盖前面的元素;因此左右半遮挡效果主要通过 Positioned 设置偏移量来完成;小菜通过定义 isCoverUp 来判断半遮挡顺序;
1.1 右侧半遮挡左侧
右侧半遮挡左侧相对较容易,仅需将数组中元素向右侧偏移固定偏移量即可;
List<Widget> _listWid = [Container(height: size)];
for (int i = 0; i < urlList.length; i++) {
_listWid.add(_itemWid(isAsset ?? false, urlList[i], i, null));
}
return Stack(children: _listWid);
1.2 左侧半遮挡右侧
左侧半遮挡右侧,小菜通过数组倒序方式,然后再将数组向右侧设置固定偏移量;
List<Widget> _listWid = [Container(height: size)];
for (int i = urlList.length - 1; i >= 0; i--) {
_listWid.add(_itemWid(isAsset ?? false, urlList[i], i, null));
}
return Stack(children: _listWid);
2. 本地图 & 网络图
小菜在自定义传递头像 URL 时考虑到,可能是网络图也可能是本地图,甚至是两者混合展示的;主要分为两类:
2.1 纯本地图 & 纯网络图
小菜设置 isAsset 为 urlList 中公共的图片属性,本地图或网络图;
if (isCoverUp ?? true) {
if (urlList != null) {
for (int i = urlList.length - 1; i >= 0; i--) {
_listWid.add(_itemWid(isAsset ?? false, urlList[i], i, null));
}
}
} else {
if (urlList != null) {
for (int i = 0; i < urlList.length; i++) {
_listWid.add(_itemWid(isAsset ?? false, urlList[i], i, null));
}
}
}
2.2 本地图片 + 网络图混合
小菜设置一个 List<Map<bool, String>> 类型的 mixUrlList,Map 中存储是否为本地图和图片地址,遍历加载时通过 bool 类型判断即可;
if (isCoverUp ?? true) {
if (mixUrlList != null) {
for (int i = mixUrlList.length - 1; i >= 0; i--) {
_listWid.add(_itemWid(mixUrlList[i].keys.first, mixUrlList[i].values.first, i, null));
}
}
} else {
if (mixUrlList != null) {
for (int i = 0; i < mixUrlList.length; i++) {
_listWid.add(_itemWid(mixUrlList[i].keys.first ?? false, mixUrlList[i].values.first, i, null));
}
}
}
3. 末尾图标
在用户头像较多点情况下,很多场景需要一个末尾省略图标,小菜提供了一个 endIcon 的 Widget 以供自定义图标的样式,可以是图片或文字或其他等展示效果;值得注意的是,之前小菜设置了 右侧半遮挡左侧 和 左侧半遮挡右侧 两种效果;因此该图标不仅需要对应的偏移量,还需要针对这两种情况先后不同顺序添加在 Stack 栈内;
List<Widget> _listWid = [Container(height: size)];
if (isCoverUp ?? true) {
if (urlList != null) {
if (endIcon != null) {
_listWid.add(_itemWid(isAsset ?? false, null, urlList.length, endIcon));
}
for (int i = urlList.length - 1; i >= 0; i--) {
_listWid.add(_itemWid(isAsset ?? false, urlList[i], i, null));
}
} else if (mixUrlList != null) {
if (endIcon != null) {
_listWid.add(_itemWid(isAsset ?? false, null, mixUrlList.length, endIcon));
}
for (int i = mixUrlList.length - 1; i >= 0; i--) {
_listWid.add(_itemWid(mixUrlList[i].keys.first, mixUrlList[i].values.first, i, null));
}
}
} else {
if (urlList != null) {
for (int i = 0; i < urlList.length; i++) {
_listWid.add(_itemWid(isAsset ?? false, urlList[i], i, null));
}
if (endIcon != null) {
_listWid.add(_itemWid(isAsset ?? false, null, urlList.length, endIcon));
}
} else if (mixUrlList != null) {
for (int i = 0; i < mixUrlList.length; i++) {
_listWid.add(_itemWid(mixUrlList[i].keys.first ?? false, mixUrlList[i].values.first, i, null));
}
if (endIcon != null) {
_listWid.add(_itemWid(isAsset ?? false, null, mixUrlList.length, endIcon));
}
}
}
return Stack(children: _listWid);
4. 自定义 Border
对于个性化需求,是否需要边框,以及边框颜色和粗细,这些属于相对简单边缘的功能点;小菜予以补充,添加了 isBorder、borderColor 和 borderWidth 属性;
return Positioned(
left: (spaceWidth ?? _kSpaceWidth) * index,
child: Container(
width: size, height: size,
decoration: BoxDecoration(
border: (isBorder ?? false) == false ? null : Border.all(color: borderColor ?? _kBorderColor, width: borderWidth ?? _kBorderWidth),
color: Colors.grey, shape: BoxShape.circle),
child: PhysicalModel(
color: Colors.transparent, shape: BoxShape.circle,
clipBehavior: Clip.antiAlias, borderRadius: BorderRadius.all(Radius.circular(20.0)),
child: Container(width: size, height: size,
child: endIcon ?? (asset ? Image.asset(url) : Image.network(url))))));
CircleAvatar
小菜在设置圆形头像时了解到 CircleAvatar 组件,Flutter 提供了很多绘制圆形的方法,小菜趁此机会简单学习一下 CircleAvatar;CircleAvatar 比较特殊,通常用于用户图片和内容一同展示,且为了保持效果一致,给定用户的姓名缩写应始终与相同的背景色配对;
源码分析
const CircleAvatar({
Key key,
this.child, // 子 Widget
this.backgroundColor, // 背景色
this.backgroundImage, // 背景图
this.foregroundColor, // 子 Widget 颜色
this.radius, // 半径
this.minRadius, // 最小半径
this.maxRadius, // 最大半径
})
简单分析源码可得,主要是通过 BoxConstraints 来限制最大最小半径,而 backgroundImage 来设置背景图;
案例尝试
1. child
child 为 CircleAvatar 中居中展示的子 Widget,一般是 TextView,用于展示姓名等;若设置图片则不会进行圆形裁切;
return CircleAvatar(radius: 40.0, child: Text(index == 0 ? 'ACE' : 'Hi'));
2. backgroundImage
backgroundImage 为 CircleAvatar 图片背景,默认居中裁切,注意 backgroundImage 对应的是 ImageProvider 而非 Widget,因此加载图片时只能采用 AssetImage 或 NetworkImage 方式;
return CircleAvatar(
radius: 40.0,
backgroundImage: index != 0
? NetworkImage('https://locadeserta.com/game/assets/images/background/landing/1.jpg')
: AssetImage('images/icon_hzw01.jpg'));
3. backgroundColor & foregroundColor
backgroundColor 和 foregroundColor 分别对应背景颜色和子 child 颜色,除非两个颜色均设置,否则两个背景色会之间会自动匹配;默认 backgroundColor 对应 Theme 的主题颜色;
return CircleAvatar(
radius: 40.0,
child: Text(index == 0 ? 'ACE' : 'Hi'),
backgroundColor: (index == 0) ? null : Colors.deepOrange,
foregroundColor: (index == 0) ? null : Colors.blue);
4. radius & minRadius & maxRadius
了解源码可得,CircleAvatar 是通过 BoxConstraints 来限制半径范围的;若设置 radius 则其余两个不生效;默认 minRadius 为 20 像素密度;
return CircleAvatar(
maxRadius: 50.0,
child: Text('ACE'),
backgroundColor: Colors.deepOrange,
foregroundColor: Colors.white,
backgroundImage: AssetImage('images/icon_hzw01.jpg'));
SeriesCircleProfile & CircleAvatar 案例源码
小菜对于系列折叠头像的自定义较为简单,没有使用复杂的 Canvas 绘制,而是通过 Stack 和 Positioned 进行展示,逻辑更为简单;如有错误,请多多指导!
来源: 阿策小和尚