Flutter-實現(xiàn)仿微信群頭像功能

Flutter實現(xiàn)仿微信群頭像功能

需求

在Flutter項目中,實現(xiàn)一個類似于微信群頭像的控件预麸。該控件能夠顯示多個頭像圖片侣姆,并且根據(jù)圖片的數(shù)量生真,自動調(diào)整布局,確保每個圖片都是正方形捺宗,且有統(tǒng)一的邊框和間距柱蟀。

效果

image.png

該控件能夠?qū)崿F(xiàn)以下效果:

  1. 根據(jù)圖片數(shù)量,動態(tài)調(diào)整圖片布局蚜厉。
  2. 每個圖片都有統(tǒng)一的邊框和間距长已。
  3. 圖片顯示為正方形,并支持圓角效果。
  4. 使用網(wǎng)絡(luò)圖片术瓮,并支持緩存和占位符康聂。

實現(xiàn)思路

  1. 控件設(shè)計:創(chuàng)建一個AvatarGroup控件,接受圖片URL列表和一些布局參數(shù)胞四。
  2. 布局計算:根據(jù)圖片數(shù)量恬汁,動態(tài)計算每個圖片的尺寸和位置。
  3. 圖片顯示:使用CachedNetworkImage加載網(wǎng)絡(luò)圖片撬讽,支持緩存蕊连、占位符和錯誤顯示。
  4. 邊框和圓角:使用ContainerBoxDecoration實現(xiàn)統(tǒng)一的邊框和圓角效果游昼。

實現(xiàn)代碼

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';

class AvatarGroup extends StatelessWidget {
  final List<String> imageUrls;
  final double size;
  final double space;
  final Color color;
  final Color borderColor;
  final double borderWidth;
  final double borderRadius;

  const AvatarGroup({
    Key? key,
    required this.imageUrls,
    this.size = 150.0,
    this.space = 4.0,
    this.color = Colors.grey,
    this.borderWidth = 3.0,
    this.borderColor = Colors.grey,
    this.borderRadius = 4.0,
  }) : super(key: key);

  double get width {
    return size - borderWidth * 2;
  }

  int get itemCount {
    return imageUrls.length;
  }

  double get itemWidth {
    if (itemCount == 1) {
      return width;
    } else if (itemCount >= 2 && itemCount <= 4) {
      return (width - space) / 2;
    } else {
      return (width - 2 * space) / 3;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: color,
        border: Border.all(color: borderColor, width: borderWidth),
        borderRadius: BorderRadius.circular(borderRadius),
      ),
      width: size,
      height: size,
      child: Stack(
        children: _buildAvatarStack(),
      ),
    );
  }

  List<Widget> _buildAvatarStack() {
    List<Widget> avatars = [];
    for (int i = 0; i < imageUrls.length; i++) {
      double left = 0;
      double top = 0;
      if (itemCount == 1) {
        left = 0;
        top = 0;
      } else if (itemCount == 2) {
        left = i * itemWidth + i * space;
        top = (width - itemWidth) / 2;
      } else if (itemCount == 3) {
        if (i == 0) {
          left = (width - itemWidth) / 2;
          top = 0;
        } else {
          left = (i - 1) * itemWidth + (i - 1) * space;
          top = itemWidth + space;
        }
      } else if (itemCount == 4) {
        if (i == 0 || i == 1) {
          left = i * itemWidth + i * space;
          top = 0;
        } else {
          left = (i - 2) * itemWidth + (i - 2) * space;
          top = itemWidth + space;
        }
      } else if (itemCount == 5) {
        if (i == 0 || i == 1) {
          left =
              (width - itemWidth * 2 - space) / 2 + i * itemWidth + i * space;
          top = (width - itemWidth * 2 - space) / 2;
        } else {
          left = (i - 2) * itemWidth + (i - 2) * space;
          top = (width - itemWidth * 2 - space) / 2 + itemWidth + space;
        }
      } else if (itemCount == 6) {
        var topOffset = (width - 2 * itemWidth - space) / 2;
        left = (i % 3) * itemWidth + (i % 3) * space;
        top = topOffset + (i / 3).floor() * itemWidth + (i / 3).floor() * space;
      } else if (itemCount == 7) {
        if (i == 0) {
          left = (width - itemWidth) / 2;
          top = 0;
        } else {
          left = ((i - 1) % 3) * itemWidth + ((i - 1) % 3) * space;
          top = itemWidth +
              space +
              ((i - 1) / 3).floor() * itemWidth +
              ((i - 1) / 3).floor() * space;
        }
      } else if (itemCount == 8) {
        if (i == 0 || i == 1) {
          left =
              (width - itemWidth * 2 - space) / 2 + i * itemWidth + i * space;
          top = 0;
        } else {
          left = ((i - 2) % 3) * itemWidth + ((i - 2) % 3) * space;
          top = itemWidth +
              space +
              ((i - 2) / 3).floor() * itemWidth +
              ((i - 2) / 3).floor() * space;
        }
      } else if (itemCount == 9) {
        left = (i % 3) * itemWidth + (i % 3) * space;
        top = (i / 3).floor() * itemWidth + (i / 3).floor() * space;
      }

      avatars.add(Positioned(
        left: left,
        top: top,
        child: ClipRect(
          child: CachedNetworkImage(
            imageUrl: imageUrls[i],
            placeholder: (context, url) => const CircularProgressIndicator(),
            errorWidget: (context, url, error) => const Icon(Icons.error),
            width: itemWidth,
            height: itemWidth,
            fit: BoxFit.cover,
          ),
        ),
      ));
    }

    return avatars;
  }
}

使用

import 'package:flutter/material.dart';
import 'package:flutter_xy/xydemo/image/avatar/avatar_grid.dart';

import '../../../widgets/xy_app_bar.dart';

class AvatarGridPage extends StatelessWidget {
  AvatarGridPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: XYAppBar(
        title: "微信群頭像",
        onBack: () {
          Navigator.pop(context);
        },
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: GridView.builder(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            crossAxisSpacing: 8.0,
            mainAxisSpacing: 8.0,
          ),
          itemCount: 9,
          itemBuilder: (context, index) {
            return LayoutBuilder(builder: (context, constraints) {
              return AvatarGroup(
                size: constraints.maxWidth,
                color: Colors.grey.withAlpha(50),
                borderColor: Colors.redAccent.withAlpha(80),
                borderWidth: 2,
                imageUrls: List.generate(
                    index + 1, (i) => _imageUrls[i % _imageUrls.length]),
              );
            });
          },
        ),
      ),
    );
  }

  final List<String> _imageUrls = [
    'https://files.mdnice.com/user/34651/0d938792-603e-4945-a1d8-e53e605693d8.jpeg',
    'https://files.mdnice.com/user/34651/a3b1fd72-ef80-4e31-8a33-2a57c4d115ce.jpeg',
    'https://files.mdnice.com/user/34651/06de6046-bf3a-454c-a75b-6beeba78408b.jpeg',
    'https://files.mdnice.com/user/34651/010ac7cb-9aa9-4a4d-93bb-14dc2cc0d994.jpeg',
    'https://files.mdnice.com/user/34651/d88604e3-0dae-46f0-ab3f-d9e016516401.jpeg',
    'https://files.mdnice.com/user/34651/91a53974-7bf7-47ba-a303-40d5fb61e31f.jpeg',
    'https://files.mdnice.com/user/34651/6b7ca51c-65d0-4f35-b5c1-2c17ef494fd8.jpeg',
    'https://files.mdnice.com/user/34651/0fbdd801-66d8-487c-bcdd-9afcaa611541.jpeg',
    'https://files.mdnice.com/user/34651/0fbdd801-66d8-487c-bcdd-9afcaa611541.jpeg',
  ];
}

結(jié)束語

通過上述實現(xiàn)甘苍,我們成功地在Flutter中創(chuàng)建了一個仿微信群頭像的控件。這個控件不僅可以動態(tài)調(diào)整布局烘豌,還支持網(wǎng)絡(luò)圖片的緩存和占位符顯示载庭。希望這篇文章對你在Flutter項目中實現(xiàn)類似功能有所幫助。如果你有任何問題或建議廊佩,歡迎訪問我的GitHub項目:github.com/yixiaolunhui/flutter_xy與我交流囚聚。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市标锄,隨后出現(xiàn)的幾起案子顽铸,更是在濱河造成了極大的恐慌,老刑警劉巖料皇,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谓松,死亡現(xiàn)場離奇詭異,居然都是意外死亡践剂,警方通過查閱死者的電腦和手機鬼譬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逊脯,“玉大人优质,你說我怎么就攤上這事【荩” “怎么了巩螃?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長匕争。 經(jīng)常有香客問我牺六,道長,這世上最難降的妖魔是什么汗捡? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上扇住,老公的妹妹穿的比我還像新娘春缕。我一直安慰自己,他們只是感情好艘蹋,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布锄贼。 她就那樣靜靜地躺著,像睡著了一般女阀。 火紅的嫁衣襯著肌膚如雪宅荤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天浸策,我揣著相機與錄音冯键,去河邊找鬼。 笑死庸汗,一個胖子當著我的面吹牛惫确,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蚯舱,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼改化,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了枉昏?” 一聲冷哼從身側(cè)響起陈肛,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎兄裂,沒想到半個月后句旱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡懦窘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年前翎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片畅涂。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡港华,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出午衰,到底是詐尸還是另有隱情立宜,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布臊岸,位于F島的核電站橙数,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帅戒。R本人自食惡果不足惜灯帮,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一崖技、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钟哥,春花似錦迎献、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至播演,卻和暖如春冀瓦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背翼闽。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工顶霞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留选浑,地道東北人古徒。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓代态,卻偏偏與公主長得像蹦疑,于是被迫代替她去往敵國和親萨驶。 傳聞我的和親對象是個殘疾皇子腔呜,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容