在这里插入图片描述

家庭树功能以可视化的方式展示家庭成员之间的关系,让用户可以直观地了解家族结构。通过树形图的形式,用户可以清楚地看到每个成员的位置和关系。今天我们来实现这个功能。

设计思路

家庭树页面采用可缩放和拖动的画布,用户可以自由浏览整个家庭树。每个成员用圆形头像展示,成员之间用线条连接表示关系。点击成员可以查看详情或编辑信息。

创建页面结构

先搭建基本框架:

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
import '../providers/family_provider.dart';
import '../models/family_member.dart';
import 'member_detail_screen.dart';

class FamilyTreeScreen extends StatefulWidget {
  const FamilyTreeScreen({super.key});

  
  State<FamilyTreeScreen> createState() => _FamilyTreeScreenState();
}

class _FamilyTreeScreenState extends State<FamilyTreeScreen> {
  final TransformationController _transformController = TransformationController();
  double _scale = 1.0;

  
  void dispose() {
    _transformController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('家庭树'),
        elevation: 0,
        actions: [
          IconButton(
            icon: const Icon(Icons.zoom_in),
            onPressed: _zoomIn,
          ),
          IconButton(
            icon: const Icon(Icons.zoom_out),
            onPressed: _zoomOut,
          ),
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: _resetView,
          ),
        ],
      ),
      body: Consumer<FamilyProvider>(
        builder: (context, provider, _) {
          final members = provider.familyMembers;
          
          if (members.isEmpty) {
            return _buildEmptyState();
          }
          
          return InteractiveViewer(
            transformationController: _transformController,
            minScale: 0.5,
            maxScale: 3.0,
            boundaryMargin: EdgeInsets.all(200.w),
            onInteractionUpdate: (details) {
              setState(() {
                _scale = _transformController.value.getMaxScaleOnAxis();
              });
            },
            child: Center(
              child: _buildFamilyTree(members),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddMemberDialog(),
        backgroundColor: const Color(0xFFE91E63),
        child: const Icon(Icons.person_add),
      ),
    );
  }
}

用InteractiveViewer实现缩放和拖动功能,TransformationController控制变换状态。AppBar上有缩放和重置按钮,方便用户操作。

空状态提示

当还没有家人时显示提示:

Widget _buildEmptyState() {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(
          Icons.account_tree_outlined,
          size: 80.sp,
          color: Colors.grey[300],
        ),
        SizedBox(height: 20.h),
        Text(
          '还没有家庭成员',
          style: TextStyle(
            fontSize: 18.sp,
            color: Colors.grey[600],
            fontWeight: FontWeight.w500,
          ),
        ),
        SizedBox(height: 8.h),
        Text(
          '点击右下角按钮添加第一位家人',
          style: TextStyle(
            fontSize: 14.sp,
            color: Colors.grey[400],
          ),
        ),
        SizedBox(height: 24.h),
        ElevatedButton.icon(
          onPressed: () => _showAddMemberDialog(),
          icon: const Icon(Icons.person_add),
          label: const Text('添加家人'),
          style: ElevatedButton.styleFrom(
            backgroundColor: const Color(0xFFE91E63),
            padding: EdgeInsets.symmetric(
              horizontal: 24.w,
              vertical: 12.h,
            ),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(24.r),
            ),
          ),
        ),
      ],
    ),
  );
}

空状态用树形图标和提示文字,还有一个添加按钮引导用户开始构建家庭树。

缩放控制

实现缩放和重置功能:

void _zoomIn() {
  final currentScale = _transformController.value.getMaxScaleOnAxis();
  if (currentScale < 3.0) {
    final newScale = (currentScale * 1.2).clamp(0.5, 3.0);
    _transformController.value = Matrix4.identity()..scale(newScale);
    setState(() {
      _scale = newScale;
    });
  }
}

void _zoomOut() {
  final currentScale = _transformController.value.getMaxScaleOnAxis();
  if (currentScale > 0.5) {
    final newScale = (currentScale / 1.2).clamp(0.5, 3.0);
    _transformController.value = Matrix4.identity()..scale(newScale);
    setState(() {
      _scale = newScale;
    });
  }
}

void _resetView() {
  _transformController.value = Matrix4.identity();
  setState(() {
    _scale = 1.0;
  });
}

缩放按钮每次放大或缩小20%,限制在0.5到3倍之间。重置按钮恢复到初始状态。

构建家庭树

根据成员关系构建树形结构:

Widget _buildFamilyTree(List<FamilyMember> members) {
  final generations = _organizeByGeneration(members);
  
  return SingleChildScrollView(
    scrollDirection: Axis.horizontal,
    child: Padding(
      padding: EdgeInsets.all(40.w),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: generations.entries.map((entry) {
          return Padding(
            padding: EdgeInsets.symmetric(vertical: 20.h),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: entry.value.map((member) {
                return Padding(
                  padding: EdgeInsets.symmetric(horizontal: 16.w),
                  child: _buildMemberNode(member),
                );
              }).toList(),
            ),
          );
        }).toList(),
      ),
    ),
  );
}

Map<int, List<FamilyMember>> _organizeByGeneration(List<FamilyMember> members) {
  final Map<int, List<FamilyMember>> generations = {};
  
  for (final member in members) {
    final generation = _getGeneration(member.relationship);
    generations.putIfAbsent(generation, () => []);
    generations[generation]!.add(member);
  }
  
  final sortedKeys = generations.keys.toList()..sort();
  return Map.fromEntries(
    sortedKeys.map((key) => MapEntry(key, generations[key]!)),
  );
}

int _getGeneration(String relationship) {
  if (relationship.contains('祖') || relationship.contains('外祖')) {
    return 0;
  } else if (relationship.contains('父') || relationship.contains('母') ||
             relationship.contains('叔') || relationship.contains('姨')) {
    return 1;
  } else if (relationship.contains('兄') || relationship.contains('姐') ||
             relationship.contains('弟') || relationship.contains('妹') ||
             relationship.contains('配偶')) {
    return 2;
  } else if (relationship.contains('子') || relationship.contains('女')) {
    return 3;
  }
  return 2;
}

根据关系类型把成员分到不同的代际,祖辈在最上面,子女在最下面。每一代的成员横向排列。

成员节点

每个成员用圆形头像展示:

Widget _buildMemberNode(FamilyMember member) {
  return GestureDetector(
    onTap: () {
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (_) => MemberDetailScreen(member: member),
        ),
      );
    },
    onLongPress: () => _showMemberOptions(member),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          width: 70.w,
          height: 70.w,
          decoration: BoxDecoration(
            color: _getColorForMember(member.avatar),
            shape: BoxShape.circle,
            border: Border.all(
              color: Colors.white,
              width: 3,
            ),
            boxShadow: [
              BoxShadow(
                color: _getColorForMember(member.avatar).withOpacity(0.3),
                blurRadius: 10,
                offset: const Offset(0, 4),
              ),
            ],
          ),
          child: Center(
            child: Text(
              member.name.substring(0, 1),
              style: TextStyle(
                fontSize: 28.sp,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
          ),
        ),
        SizedBox(height: 8.h),
        Text(
          member.name,
          style: TextStyle(
            fontSize: 14.sp,
            fontWeight: FontWeight.w600,
            color: Colors.black87,
          ),
        ),
        SizedBox(height: 2.h),
        Container(
          padding: EdgeInsets.symmetric(
            horizontal: 8.w,
            vertical: 2.h,
          ),
          decoration: BoxDecoration(
            color: const Color(0xFFE91E63).withOpacity(0.1),
            borderRadius: BorderRadius.circular(8.r),
          ),
          child: Text(
            member.relationship,
            style: TextStyle(
              fontSize: 11.sp,
              color: const Color(0xFFE91E63),
            ),
          ),
        ),
      ],
    ),
  );
}

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

成员节点包含圆形头像、姓名和关系标签。头像有阴影效果,点击可以查看详情,长按显示更多选项。

成员操作菜单

长按成员显示操作选项:

void _showMemberOptions(FamilyMember member) {
  showModalBottomSheet(
    context: context,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(
        top: Radius.circular(20.r),
      ),
    ),
    builder: (sheetContext) => SafeArea(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          SizedBox(height: 8.h),
          Container(
            width: 40.w,
            height: 4.h,
            decoration: BoxDecoration(
              color: Colors.grey[300],
              borderRadius: BorderRadius.circular(2.r),
            ),
          ),
          SizedBox(height: 16.h),
          ListTile(
            leading: CircleAvatar(
              backgroundColor: _getColorForMember(member.avatar),
              child: Text(
                member.name.substring(0, 1),
                style: const TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
            title: Text(
              member.name,
              style: TextStyle(
                fontSize: 18.sp,
                fontWeight: FontWeight.bold,
              ),
            ),
            subtitle: Text(member.relationship),
          ),
          const Divider(),
          ListTile(
            leading: const Icon(Icons.info, color: Color(0xFF2196F3)),
            title: const Text('查看详情'),
            onTap: () {
              Navigator.pop(sheetContext);
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => MemberDetailScreen(member: member),
                ),
              );
            },
          ),
          ListTile(
            leading: const Icon(Icons.edit, color: Color(0xFF4CAF50)),
            title: const Text('编辑信息'),
            onTap: () {
              Navigator.pop(sheetContext);
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => EditMemberScreen(member: member),
                ),
              );
            },
          ),
          ListTile(
            leading: const Icon(Icons.link, color: Color(0xFFFF9800)),
            title: const Text('设置关系'),
            onTap: () {
              Navigator.pop(sheetContext);
              _showRelationshipDialog(member);
            },
          ),
          ListTile(
            leading: const Icon(Icons.delete, color: Color(0xFFF44336)),
            title: const Text('删除'),
            onTap: () {
              Navigator.pop(sheetContext);
              _showDeleteDialog(member);
            },
          ),
          SizedBox(height: 16.h),
        ],
      ),
    ),
  );
}

底部菜单显示成员信息和操作选项,包括查看详情、编辑信息、设置关系和删除。

设置关系对话框

让用户设置成员之间的关系:

void _showRelationshipDialog(FamilyMember member) {
  final provider = context.read<FamilyProvider>();
  final otherMembers = provider.familyMembers
      .where((m) => m.id != member.id)
      .toList();

  showDialog(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: Text('设置${member.name}的关系'),
      content: SizedBox(
        width: double.maxFinite,
        child: ListView.builder(
          shrinkWrap: true,
          itemCount: otherMembers.length,
          itemBuilder: (context, index) {
            final other = otherMembers[index];
            return ListTile(
              leading: CircleAvatar(
                backgroundColor: _getColorForMember(other.avatar),
                child: Text(
                  other.name.substring(0, 1),
                  style: const TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
              title: Text(other.name),
              subtitle: Text(other.relationship),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {
                Navigator.pop(dialogContext);
                _showRelationTypeDialog(member, other);
              },
            );
          },
        ),
      ),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.r),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext),
          child: const Text('取消'),
        ),
      ],
    ),
  );
}

void _showRelationTypeDialog(FamilyMember member, FamilyMember other) {
  final relationTypes = [
    '父子', '母子', '夫妻', '兄弟', '姐妹', '祖孙', '其他'
  ];

  showDialog(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: Text('${member.name}${other.name}的关系'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: relationTypes.map((type) {
          return ListTile(
            title: Text(type),
            onTap: () {
              context.read<FamilyProvider>().setRelationship(
                member.id,
                other.id,
                type,
              );
              Navigator.pop(dialogContext);
              
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text('已设置关系:$type'),
                  behavior: SnackBarBehavior.floating,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(8.r),
                  ),
                  margin: EdgeInsets.all(16.w),
                ),
              );
            },
          );
        }).toList(),
      ),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.r),
      ),
    ),
  );
}

关系设置分两步,先选择要关联的成员,再选择关系类型。设置成功后显示提示。

删除确认对话框

删除成员前需要确认:

void _showDeleteDialog(FamilyMember member) {
  showDialog(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: Row(
        children: [
          const Icon(
            Icons.warning_amber_rounded,
            color: Color(0xFFF44336),
          ),
          SizedBox(width: 8.w),
          const Text('删除成员'),
        ],
      ),
      content: Text('确定要从家庭树中删除"${member.name}"吗?'),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.r),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext),
          child: Text(
            '取消',
            style: TextStyle(color: Colors.grey[600]),
          ),
        ),
        TextButton(
          onPressed: () {
            context.read<FamilyProvider>().deleteMember(member.id);
            Navigator.pop(dialogContext);
            
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text('已删除"${member.name}"'),
                backgroundColor: const Color(0xFFF44336),
                behavior: SnackBarBehavior.floating,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(8.r),
                ),
                margin: EdgeInsets.all(16.w),
              ),
            );
          },
          child: const Text(
            '删除',
            style: TextStyle(
              color: Color(0xFFF44336),
              fontWeight: FontWeight.w600,
            ),
          ),
        ),
      ],
    ),
  );
}

删除对话框有警告图标,删除按钮用红色表示危险操作。

添加成员对话框

快速添加新成员:

void _showAddMemberDialog() {
  final nameController = TextEditingController();
  String selectedRelationship = '其他';

  showDialog(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: const Text('添加家人'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: nameController,
            decoration: InputDecoration(
              labelText: '姓名',
              hintText: '请输入姓名',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.r),
              ),
            ),
            autofocus: true,
          ),
          SizedBox(height: 16.h),
          DropdownButtonFormField<String>(
            value: selectedRelationship,
            decoration: InputDecoration(
              labelText: '关系',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.r),
              ),
            ),
            items: [
              '父亲', '母亲', '祖父', '祖母', '儿子', '女儿',
              '兄弟', '姐妹', '配偶', '其他'
            ].map((r) => DropdownMenuItem(
              value: r,
              child: Text(r),
            )).toList(),
            onChanged: (value) {
              selectedRelationship = value!;
            },
          ),
        ],
      ),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.r),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext),
          child: Text(
            '取消',
            style: TextStyle(color: Colors.grey[600]),
          ),
        ),
        TextButton(
          onPressed: () {
            if (nameController.text.trim().isNotEmpty) {
              final member = FamilyMember(
                id: DateTime.now().millisecondsSinceEpoch.toString(),
                name: nameController.text.trim(),
                relationship: selectedRelationship,
                avatar: nameController.text.trim(),
              );
              context.read<FamilyProvider>().addMember(member);
              Navigator.pop(dialogContext);
              
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text('已添加"${member.name}"'),
                  behavior: SnackBarBehavior.floating,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(8.r),
                  ),
                  margin: EdgeInsets.all(16.w),
                ),
              );
            }
          },
          child: const Text(
            '添加',
            style: TextStyle(
              color: Color(0xFFE91E63),
              fontWeight: FontWeight.w600,
            ),
          ),
        ),
      ],
    ),
  );
}

添加对话框包含姓名输入框和关系下拉菜单,用户可以快速添加新成员到家庭树。

总结

家庭树页面通过可视化的方式展示家庭成员之间的关系。InteractiveViewer实现缩放和拖动功能,用户可以自由浏览整个家庭树。成员按代际分层排列,每个成员用圆形头像展示。长按成员可以查看详情、编辑信息、设置关系或删除。添加成员对话框让用户可以快速扩展家庭树。整个页面交互流畅,信息展示清晰。

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

Logo

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

更多推荐