feat(app): 创建文字输入覆盖层组件 — TextInputOverlay

This commit is contained in:
iven
2026-06-01 21:23:30 +08:00
parent 417dcb08e4
commit d392515f4a

View 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));
}
}