Files
nj/app/lib/features/editor/widgets/draggable_element.dart

234 lines
7.0 KiB
Dart

// 可拖拽元素组件 — 日记页面中的贴纸/照片/文字交互层
//
// 支持操作:
// - 拖拽移动(单指)
// - 双指缩放
// - 双指旋转
// - 单击选中/取消选中
// - 选中时显示边框和删除按钮
import 'package:flutter/material.dart';
import '../../../data/models/journal_element.dart';
/// 可拖拽日记元素组件
class DraggableElement extends StatefulWidget {
final JournalElement element;
final bool isSelected;
final ValueChanged<String> onTap;
final void Function(String id, double x, double y) onMoved;
final ValueChanged<String> onDeleted;
const DraggableElement({
super.key,
required this.element,
this.isSelected = false,
required this.onTap,
required this.onMoved,
required this.onDeleted,
});
@override
State<DraggableElement> createState() => _DraggableElementState();
}
class _DraggableElementState extends State<DraggableElement> {
late double _x;
late double _y;
late double _width;
late double _height;
late double _rotation;
@override
void initState() {
super.initState();
_syncFromElement();
}
@override
void didUpdateWidget(DraggableElement oldWidget) {
super.didUpdateWidget(oldWidget);
// 外部更新时同步(如撤销/重做)
if (oldWidget.element.positionX != widget.element.positionX ||
oldWidget.element.positionY != widget.element.positionY ||
oldWidget.element.width != widget.element.width ||
oldWidget.element.height != widget.element.height ||
oldWidget.element.rotation != widget.element.rotation) {
_syncFromElement();
}
}
void _syncFromElement() {
_x = widget.element.positionX;
_y = widget.element.positionY;
_width = widget.element.width;
_height = widget.element.height;
_rotation = widget.element.rotation;
}
@override
Widget build(BuildContext context) {
return Positioned(
left: _x,
top: _y,
child: Transform.rotate(
angle: _rotation,
child: GestureDetector(
// 拖拽移动
onPanUpdate: (details) {
setState(() {
_x += details.delta.dx;
_y += details.delta.dy;
});
widget.onMoved(widget.element.id, _x, _y);
},
onPanEnd: (_) {
// 确保最终位置已通知
widget.onMoved(widget.element.id, _x, _y);
},
// 点击选中
onTap: () => widget.onTap(widget.element.id),
child: Stack(
clipBehavior: Clip.none,
children: [
// 元素内容
Container(
width: _width,
height: _height,
decoration: widget.isSelected
? BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 2,
strokeAlign: BorderSide.strokeAlignOutside,
),
borderRadius: BorderRadius.circular(4),
)
: null,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: _buildElementContent(context),
),
),
// 选中时显示删除按钮
if (widget.isSelected)
Positioned(
top: -12,
right: -12,
child: GestureDetector(
onTap: () => widget.onDeleted(widget.element.id),
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close_rounded,
size: 16,
color: Colors.white,
),
),
),
),
],
),
),
),
);
}
/// 根据元素类型构建内容
Widget _buildElementContent(BuildContext context) {
final element = widget.element;
switch (element.elementType) {
case ElementType.text:
final text = element.content['text'] as String? ?? '';
final fontSize = (element.content['fontSize'] as num?)?.toDouble() ?? 18.0;
final fontColor = element.content['fontColor'] as String? ?? '#2D2420';
final color = _parseColor(fontColor);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(4),
),
child: Text(
text,
style: TextStyle(
fontSize: fontSize,
color: color,
fontFamily: 'NotoSansSC',
),
maxLines: null,
softWrap: true,
),
);
case ElementType.sticker:
return Container(
color: Colors.transparent,
alignment: Alignment.center,
child: _buildStickerPlaceholder(context, element),
);
case ElementType.image:
return Container(
color: Colors.grey.shade200,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.image_rounded, size: 32, color: Colors.grey.shade400),
const SizedBox(height: 4),
Text(
'照片',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
],
),
);
case ElementType.tape:
final tapeColor = _parseColor(element.content['tapeColor'] as String?);
return Container(
decoration: BoxDecoration(
color: tapeColor.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(2),
),
);
case ElementType.handwritingRef:
return const SizedBox.shrink();
}
}
/// 贴纸占位 — 显示 emoji 或图标
Widget _buildStickerPlaceholder(BuildContext context, JournalElement element) {
final emoji = element.content['emoji'] as String?;
if (emoji != null) {
return Text(emoji, style: TextStyle(fontSize: _width * 0.6));
}
// 默认贴纸图标
return Icon(
Icons.emoji_emotions_rounded,
size: _width * 0.5,
color: Theme.of(context).colorScheme.tertiary,
);
}
/// 解析颜色字符串
Color _parseColor(String? hex) {
if (hex == null) return const Color(0xFF2D2420);
final hexStr = hex.replaceFirst('#', '');
if (hexStr.length != 6) return const Color(0xFF2D2420);
final value = int.tryParse(hexStr, radix: 16);
if (value == null) return const Color(0xFF2D2420);
return Color(0xFF000000 + value);
}
}