在这里插入图片描述

做家庭相册App,家人管理是核心功能之一。

今天来聊聊家人Tab页面的实现,这个页面承载了家庭成员的展示和管理。

整体思路

家人Tab作为底部导航的第二个页面,用户切换过来第一眼看到的就是家庭成员列表。

我希望它能够清晰展示所有家人、提供快捷功能入口、支持添加新成员。

想清楚这些之后,页面的布局也就大致有了方向。

顶部放快捷操作,下面用网格展示家人列表,右下角放添加按钮。

页面框架搭建

先把基本的页面框架搭起来:

class FamilyTab extends StatelessWidget {
  const FamilyTab({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('家人'),
        actions: [
          IconButton(
            icon: const Icon(Icons.account_tree),
            onPressed: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => const FamilyTreeScreen()),
            ),
          ),
        ],
      ),

这里我用Scaffold作为页面的基础结构,这是Flutter页面的标准写法。

AppBar里放了标题"家人",简洁明了,用户一眼就知道这是什么页面。

右上角放了一个家庭树图标按钮,点击后跳转到家庭树页面,方便用户查看家庭关系结构。

使用CustomScrollView

页面主体部分我用了CustomScrollView配合Sliver系列组件:

      body: Consumer<FamilyProvider>(
        builder: (context, provider, _) {
          return CustomScrollView(
            slivers: [
              SliverToBoxAdapter(
                child: _buildQuickActions(context),
              ),

Consumer包裹是为了监听FamilyProvider的数据变化,当家人数据更新时页面会自动刷新。

CustomScrollView的好处是整个页面可以统一滚动,不会出现嵌套滚动冲突的问题。

SliverToBoxAdapter可以把普通Widget转换成Sliver,这样就能放进CustomScrollView里了。

家人列表标题

在快捷操作下面放一个标题,显示家人数量:

              SliverToBoxAdapter(
                child: Padding(
                  padding: EdgeInsets.all(16.w),
                  child: Text(
                    '家庭成员 (${provider.members.length})',
                    style: TextStyle(
                      fontSize: 18.sp,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),

标题里显示家人数量,用户不用数就知道有多少人,这是一个小细节但很实用。

字体设成18号加粗,作为区块标题足够醒目。

四周加16的内边距,和其他元素保持一致的间距。

家人网格布局

SliverGrid展示家人列表:

              SliverPadding(
                padding: EdgeInsets.symmetric(horizontal: 16.w),
                sliver: SliverGrid(
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 3,
                    mainAxisSpacing: 16.w,
                    crossAxisSpacing: 16.w,
                    childAspectRatio: 0.8,
                  ),

SliverPadding给网格加上左右内边距,不会贴着屏幕边缘。

设置三列布局,每行显示三个家人,在手机屏幕上刚好合适。

childAspectRatio设成0.8让卡片稍微高一点,这样头像和名字都能显示完整。

                  delegate: SliverChildBuilderDelegate(
                    (context, index) {
                      final member = provider.members[index];
                      return _buildMemberCard(context, member);
                    },
                    childCount: provider.members.length,
                  ),
                ),
              ),

SliverChildBuilderDelegate按需构建子项,只有可见的卡片才会被创建,性能更好。

provider.members获取家人数据,遍历构建每个家人卡片。

childCount告诉Flutter总共有多少个子项,这样滚动条才能正确显示。

底部留白

在列表底部留一些空间,避免被悬浮按钮遮挡:

              SliverToBoxAdapter(
                child: SizedBox(height: 100.h),
              ),
            ],
          );
        },
      ),

底部留100的高度,确保最后一行家人卡片不会被悬浮按钮挡住。

这是一个容易忽略的细节,但对用户体验影响很大。

悬浮添加按钮

右下角放一个悬浮按钮用来添加家人:

      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.push(
          context,
          MaterialPageRoute(builder: (_) => const AddMemberScreen()),
        ),
        child: const Icon(Icons.person_add),
      ),
    );
  }

FloatingActionButton是Material Design的标准组件,用户对这个位置和样式很熟悉。

person_add图标,一看就知道是添加人员的功能。

点击后跳转到添加家人页面,交互很直观。

快捷操作区域

我加了两个快捷入口卡片,分别是家庭分组和家庭回忆:

  Widget _buildQuickActions(BuildContext context) {
    return Padding(
      padding: EdgeInsets.all(16.w),
      child: Row(
        children: [
          Expanded(
            child: _buildActionCard(
              context,
              icon: Icons.groups,
              title: '家庭分组',
              color: Colors.blue,
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(builder: (_) => const GroupListScreen()),
              ),
            ),
          ),

RowExpanded让两个卡片平分宽度,在不同屏幕尺寸下都能自适应。

家庭分组用蓝色,可以按小家庭来管理成员。

点击后跳转到分组列表页面。

          SizedBox(width: 12.w),
          Expanded(
            child: _buildActionCard(
              context,
              icon: Icons.auto_stories,
              title: '家庭回忆',
              color: Colors.purple,
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(builder: (_) => const MemoryListScreen()),
              ),
            ),
          ),
        ],
      ),
    );
  }

中间的SizedBox控制两个卡片之间的间距,12的宽度刚好合适。

家庭回忆用紫色,和分组形成视觉区分。

书本图标暗示"故事"和"回忆"的含义。

快捷卡片组件

把卡片的构建抽成单独的方法,避免重复代码:

  Widget _buildActionCard(
    BuildContext context, {
    required IconData icon,
    required String title,
    required Color color,
    required VoidCallback onTap,
  }) {
    return InkWell(
      onTap: onTap,
      child: Container(
        padding: EdgeInsets.all(16.w),
        decoration: BoxDecoration(
          color: color.withOpacity(0.1),
          borderRadius: BorderRadius.circular(12.r),
        ),

背景色用主色调的10%透明度,这样既有颜色区分又不会太抢眼。

InkWell包裹让点击有水波纹效果,给用户反馈。

圆角设成12,和整体风格统一。

        child: Row(
          children: [
            Icon(icon, color: color, size: 24.sp),
            SizedBox(width: 8.w),
            Text(
              title,
              style: TextStyle(
                fontSize: 14.sp,
                fontWeight: FontWeight.w500,
                color: color,
              ),
            ),
          ],
        ),
      ),
    );
  }

图标和文字用同一个颜色,保持视觉一致性。

字体稍微加粗一点,让标题更醒目。

图标和文字水平排列,紧凑但不拥挤。

家人卡片设计

家人卡片是这个页面最重要的部分:

  Widget _buildMemberCard(BuildContext context, FamilyMember member) {
    return InkWell(
      onTap: () => Navigator.push(
        context,
        MaterialPageRoute(builder: (_) => MemberDetailScreen(member: member)),
      ),
      child: Column(
        children: [
          Container(
            width: 70.w,
            height: 70.w,
            decoration: BoxDecoration(
              color: _getColorForMember(member.avatar),
              shape: BoxShape.circle,
            ),

点击整个卡片都能跳转到家人详情页,把member对象传过去。

头像用圆形容器,70的尺寸在三列布局下刚好合适。

背景色根据家人动态生成,每个人颜色不同。

            child: Center(
              child: Text(
                member.name.substring(0, 1),
                style: TextStyle(
                  fontSize: 28.sp,
                  fontWeight: FontWeight.bold,
                  color: Colors.white,
                ),
              ),
            ),
          ),

头像里显示名字的第一个字,比如"爸"、“妈”,一目了然。

白色文字在彩色背景上很清晰,对比度足够。

字体28号加粗,在70的圆形里大小合适。

          SizedBox(height: 8.h),
          Text(
            member.name,
            style: TextStyle(
              fontSize: 14.sp,
              fontWeight: FontWeight.w500,
            ),
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),

头像下面显示完整名字,限制一行超出省略。

字体稍微加粗,和关系文字形成层次。

          Text(
            member.relationship,
            style: TextStyle(
              fontSize: 12.sp,
              color: Colors.grey,
            ),
          ),
        ],
      ),
    );
  }

关系用灰色小字显示,比如"父亲"、“母亲”,作为辅助信息。

12号字体比名字小一号,形成主次关系。

颜色映射方法

颜色根据家人的avatar字段哈希值来选:

  Color _getColorForMember(String avatar) {
    final colors = [
      const Color(0xFFE91E63),
      const Color(0xFF9C27B0),
      const Color(0xFF3F51B5),
      const Color(0xFF2196F3),
      const Color(0xFF009688),
      const Color(0xFF4CAF50),
      const Color(0xFFFF9800),
      const Color(0xFFFF5722),
    ];
    return colors[avatar.hashCode % colors.length];
  }
}

准备了8种颜色,用哈希值取模来选择,保证同一个人每次打开颜色都固定。

这些颜色都是Material Design的标准色,饱和度适中,作为头像背景很合适。

不同家人大概率颜色不同,视觉上容易区分。

数据流转

家人数据从FamilyProvider获取:

Consumer<FamilyProvider>(
  builder: (context, provider, _) {
    // 使用 provider.members
  },
)

Consumer会在FamilyProvider数据变化时自动重建UI。

添加或删除家人后,列表会自动更新,不需要手动刷新。

小结

家人Tab页面的核心就是这些了。

CustomScrollView统一管理滚动,快捷操作提供常用功能入口。

家人卡片用颜色加首字的方式展示,简洁又有辨识度。

下一篇会写家人详情页的实现。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐