feat(app): 创建文字输入覆盖层组件 — TextInputOverlay
This commit is contained in:
205
app/lib/features/editor/widgets/text_input_overlay.dart
Normal file
205
app/lib/features/editor/widgets/text_input_overlay.dart
Normal file
@@ -0,0 +1,205 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 编辑器文字输入覆盖层
|
||||
/// 当用户选择文字工具时,在画布上叠加一个 TextField
|
||||
class TextInputOverlay extends StatefulWidget {
|
||||
final void Function(String text, double fontSize, String fontColor)
|
||||
onConfirmed;
|
||||
final VoidCallback onCancelled;
|
||||
|
||||
const TextInputOverlay({
|
||||
super.key,
|
||||
required this.onConfirmed,
|
||||
required this.onCancelled,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TextInputOverlay> createState() => _TextInputOverlayState();
|
||||
}
|
||||
|
||||
class _TextInputOverlayState extends State<TextInputOverlay> {
|
||||
final _controller = TextEditingController();
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
double _fontSize = 18.0;
|
||||
String _fontColor = '#2D2420';
|
||||
|
||||
// 字号选项:小(14)/中(18)/大(24)
|
||||
static const _fontSizes = [14.0, 18.0, 24.0];
|
||||
static const _fontSizeLabels = ['小', '中', '大'];
|
||||
|
||||
// 颜色选项
|
||||
static const _colors = [
|
||||
'#2D2420', // 主文字色
|
||||
'#E07A5F', // 珊瑚色
|
||||
'#81B29A', // 鼠尾草绿
|
||||
'#2C7DA0', // 蓝色
|
||||
'#D4A5A5', // 玫瑰粉
|
||||
'#F2CC8F', // 暖金
|
||||
'#9B5DE5', // 紫色
|
||||
'#F15BB5', // 粉色
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 自动弹出键盘
|
||||
Future.microtask(() => _focusNode.requestFocus());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _confirm() {
|
||||
final text = _controller.text.trim();
|
||||
if (text.isEmpty) {
|
||||
widget.onCancelled();
|
||||
return;
|
||||
}
|
||||
widget.onConfirmed(text, _fontSize, _fontColor);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black26,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.85,
|
||||
constraints: const BoxConstraints(maxHeight: 280),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 标题行
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'添加文字',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: widget.onCancelled,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 文字输入框
|
||||
TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _confirm(),
|
||||
style: TextStyle(fontSize: _fontSize),
|
||||
decoration: InputDecoration(
|
||||
hintText: '在这里输入文字...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 字号选择
|
||||
Row(
|
||||
children: [
|
||||
Text('字号', style: Theme.of(context).textTheme.bodySmall),
|
||||
const SizedBox(width: 8),
|
||||
...List.generate(_fontSizes.length, (i) {
|
||||
final selected = _fontSize == _fontSizes[i];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ChoiceChip(
|
||||
label: Text(_fontSizeLabels[i]),
|
||||
selected: selected,
|
||||
onSelected: (_) {
|
||||
setState(() => _fontSize = _fontSizes[i]);
|
||||
},
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 颜色选择
|
||||
Row(
|
||||
children: [
|
||||
Text('颜色', style: Theme.of(context).textTheme.bodySmall),
|
||||
const SizedBox(width: 8),
|
||||
..._colors.map((hex) {
|
||||
final selected = _fontColor == hex;
|
||||
final color = _parseHexColor(hex);
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _fontColor = hex),
|
||||
child: Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: selected
|
||||
? Border.all(color: Colors.black87, width: 2.5)
|
||||
: Border.all(
|
||||
color: Colors.grey.shade300, width: 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 确认按钮
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
child: FilledButton(
|
||||
onPressed: _confirm,
|
||||
child: const Text('添加到日记'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _parseHexColor(String hex) {
|
||||
final code = hex.replaceAll('#', '');
|
||||
return Color(int.parse('FF$code', radix: 16));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user