小菜在學習過程中,想實現一行半遮擋的用戶頭像的功能,橫向展示過程中,具體包括 右側頭像逐個半遮擋左側頭像 和 左側頭像逐個半遮擋右側頭像 兩種展示;
- 可展示本地圖或網絡圖;
- 可自定義末尾圖標;
- 可自定義邊框樣式;
整個自定義過程主要是通過基礎的 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 進行展示,邏輯更爲簡單;如有錯誤,請多多指導!
來源: 阿策小和尚