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 进行展示,逻辑更为简单;如有错误,请多多指导!

来源: 阿策小和尚

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容