// 可拖拽元素组件 — 日记页面中的贴纸/照片/文字交互层 // // 支持操作: // - 拖拽移动(单指) // - 双指缩放 // - 双指旋转 // - 单击选中/取消选中 // - 选中时显示边框和删除按钮 import 'dart:io'; import 'package:flutter/material.dart'; import '../../../data/models/journal_element.dart'; import '../bloc/editor_bloc.dart' show LayerChange; /// 可拖拽日记元素组件 class DraggableElement extends StatefulWidget { final JournalElement element; final bool isSelected; final ValueChanged onTap; final void Function(String id, double x, double y) onMoved; final void Function(String id, double w, double h)? onResized; final void Function(String id, double rotation)? onRotated; final ValueChanged onDeleted; final void Function(String id, LayerChange change)? onLayerChanged; const DraggableElement({ super.key, required this.element, this.isSelected = false, required this.onTap, required this.onMoved, this.onResized, this.onRotated, required this.onDeleted, this.onLayerChanged, }); @override State createState() => _DraggableElementState(); } class _DraggableElementState extends State { late double _x; late double _y; late double _width; late double _height; late double _rotation; // Scale 手势状态 double _baseWidth = 0; double _baseHeight = 0; double _baseRotation = 0; @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( // 缩放开始 — 记录基准值 onScaleStart: (details) { _baseWidth = _width; _baseHeight = _height; _baseRotation = _rotation; }, // 缩放更新 — 支持单指拖拽 + 双指缩放/旋转 onScaleUpdate: (details) { setState(() { // 拖拽(单指和双指都支持) _x += details.focalPointDelta.dx; _y += details.focalPointDelta.dy; // 双指缩放 + 旋转 if (details.pointerCount >= 2) { final newW = (_baseWidth * details.scale).clamp(40.0, 400.0); final newH = (_baseHeight * details.scale).clamp(40.0, 400.0); _width = newW; _height = newH; _rotation = _baseRotation + details.rotation; } }); widget.onMoved(widget.element.id, _x, _y); if (details.pointerCount >= 2) { widget.onResized?.call(widget.element.id, _width, _height); widget.onRotated?.call(widget.element.id, _rotation); } }, onScaleEnd: (_) { // 确保最终位置已通知 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: Row( mainAxisSize: MainAxisSize.min, children: [ // 置顶 _ActionButton( icon: Icons.flip_to_front_rounded, color: Theme.of(context).colorScheme.primary, onTap: () => widget.onLayerChanged?.call( widget.element.id, LayerChange.bringToFront, ), ), const SizedBox(width: 4), // 置底 _ActionButton( icon: Icons.flip_to_back_rounded, color: Theme.of(context).colorScheme.primary, onTap: () => widget.onLayerChanged?.call( widget.element.id, LayerChange.sendToBack, ), ), const SizedBox(width: 4), // 删除 _ActionButton( icon: Icons.close_rounded, color: Theme.of(context).colorScheme.error, onTap: () => widget.onDeleted(widget.element.id), ), ], ), ), ], ), ), ), ); } /// 根据元素类型构建内容 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: final filePath = element.content['filePath'] as String?; if (filePath != null && filePath.isNotEmpty) { return ClipRRect( borderRadius: BorderRadius.circular(4), child: Image.file( File(filePath), fit: BoxFit.contain, errorBuilder: (_, __, ___) => _buildImagePlaceholder(), ), ); } return _buildImagePlaceholder(); 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); } /// 图片加载失败时的占位符 Widget _buildImagePlaceholder() { return Container( width: 160, height: 120, decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(4), ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.broken_image_outlined, color: Colors.grey), SizedBox(height: 4), Text('图片加载失败', style: TextStyle(color: Colors.grey, fontSize: 11)), ], ), ); } } /// 选中元素的操作按钮(图层/删除) class _ActionButton extends StatelessWidget { final IconData icon; final Color color; final VoidCallback onTap; const _ActionButton({ required this.icon, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: 24, height: 24, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), child: Icon(icon, size: 16, color: Colors.white), ), ); } }