Flutter 122: 圖解自定義半遮擋頭像 SeriesCircleProfile & CircleAvatar

    小菜在學習過程中,想實現一行半遮擋的用戶頭像的功能,橫向展示過程中,具體包括 右側頭像逐個半遮擋左側頭像左側頭像逐個半遮擋右側頭像 兩種展示;

  • 可展示本地圖或網絡圖;
  • 可自定義末尾圖標;
  • 可自定義邊框樣式;

    整個自定義過程主要是通過基礎的 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 純本地圖 & 純網絡圖

    小菜設置 isAsseturlList 中公共的圖片屬性,本地圖或網絡圖;

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>> 類型的 mixUrlListMap 中存儲是否爲本地圖和圖片地址,遍歷加載時通過 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. 末尾圖標

    在用戶頭像較多點情況下,很多場景需要一個末尾省略圖標,小菜提供了一個 endIconWidget 以供自定義圖標的樣式,可以是圖片或文字或其他等展示效果;值得注意的是,之前小菜設置了 右側半遮擋左側左側半遮擋右側 兩種效果;因此該圖標不僅需要對應的偏移量,還需要針對這兩種情況先後不同順序添加在 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 提供了很多繪製圓形的方法,小菜趁此機會簡單學習一下 CircleAvatarCircleAvatar 比較特殊,通常用於用戶圖片和內容一同展示,且爲了保持效果一致,給定用戶的姓名縮寫應始終與相同的背景色配對;

源碼分析

const CircleAvatar({
    Key key,
    this.child,             // 子 Widget
    this.backgroundColor,   // 背景色
    this.backgroundImage,   // 背景圖
    this.foregroundColor,   // 子 Widget 顏色
    this.radius,            // 半徑
    this.minRadius,         // 最小半徑
    this.maxRadius,         // 最大半徑
})

    簡單分析源碼可得,主要是通過 BoxConstraints 來限制最大最小半徑,而 backgroundImage 來設置背景圖;

案例嘗試

1. child

    childCircleAvatar 中居中展示的子 Widget,一般是 TextView,用於展示姓名等;若設置圖片則不會進行圓形裁切;

return CircleAvatar(radius: 40.0, child: Text(index == 0 ? 'ACE' : 'Hi'));

2. backgroundImage

    backgroundImageCircleAvatar 圖片背景,默認居中裁切,注意 backgroundImage 對應的是 ImageProvider 而非 Widget,因此加載圖片時只能採用 AssetImageNetworkImage 方式;

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

    backgroundColorforegroundColor 分別對應背景顏色和子 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 則其餘兩個不生效;默認 minRadius20 像素密度;

return CircleAvatar(
    maxRadius: 50.0,
    child: Text('ACE'),
    backgroundColor: Colors.deepOrange,
    foregroundColor: Colors.white,
    backgroundImage: AssetImage('images/icon_hzw01.jpg'));

    SeriesCircleProfile & CircleAvatar 案例源碼


    小菜對於系列摺疊頭像的自定義較爲簡單,沒有使用複雜的 Canvas 繪製,而是通過 StackPositioned 進行展示,邏輯更爲簡單;如有錯誤,請多多指導!

來源: 阿策小和尚

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章