flutter_for_openharmony家庭相册app实战+家庭树实现
本文介绍了家庭树功能的实现方法,采用Flutter框架构建可视化家族关系图。主要内容包括: 页面结构设计:使用InteractiveViewer实现可缩放拖动的画布,包含缩放控制按钮和成员添加功能 空状态处理:当没有家庭成员时显示引导提示和添加按钮 核心功能实现: 通过TransformationController控制视图变换 提供放大、缩小和重置视图的操作 按辈分组织家庭成员关系 交互设计:点

家庭树功能以可视化的方式展示家庭成员之间的关系,让用户可以直观地了解家族结构。通过树形图的形式,用户可以清楚地看到每个成员的位置和关系。今天我们来实现这个功能。
设计思路
家庭树页面采用可缩放和拖动的画布,用户可以自由浏览整个家庭树。每个成员用圆形头像展示,成员之间用线条连接表示关系。点击成员可以查看详情或编辑信息。
创建页面结构
先搭建基本框架:
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
更多推荐



所有评论(0)