Files
xianyan/lib/editor/widgets/editors/gradient_editor.dart
Developer a5b997aecb feat: 新增编辑器功能与优化
1. 添加编辑器3D场景服务接口及实现
2. 实现平台IO工具类与数据库连接
3. 新增SVG图标资源
4. 优化编辑器操作Mixin架构
5. 修复Web端兼容性问题
6. 更新依赖配置与构建脚本
7. 改进主题自适应服务
8. 添加对齐辅助线组件
9. 完善迷你文字编辑栏
10. 优化页面过渡动画
2026-04-25 09:50:30 +08:00

473 lines
13 KiB
Dart

// ============================================================
// 闲言APP — 渐变自定义编辑器
// 创建时间: 2026-04-24
// 更新时间: 2026-04-24
// 作用: 多节点渐变编辑 + 角度旋转 + 实时预览
// 上次更新: 初始创建
// ============================================================
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:xianyan/core/theme/app_colors.dart';
import 'package:xianyan/editor/widgets/editors/color_picker.dart';
/// 渐变节点
class GradientStop {
const GradientStop({required this.color, required this.position});
final Color color;
final double position;
GradientStop copyWith({Color? color, double? position}) {
return GradientStop(
color: color ?? this.color,
position: position ?? this.position,
);
}
}
/// 渐变编辑器
class GradientEditor extends StatefulWidget {
const GradientEditor({
super.key,
required this.colors,
required this.stops,
required this.angle,
required this.onChanged,
required this.onAngleChanged,
this.onDone,
});
final List<Color> colors;
final List<double> stops;
final double angle;
final ValueChanged<List<GradientStop>> onChanged;
final ValueChanged<double> onAngleChanged;
final VoidCallback? onDone;
@override
State<GradientEditor> createState() => _GradientEditorState();
}
class _GradientEditorState extends State<GradientEditor> {
late List<GradientStop> _stops;
late double _angle;
int? _selectedStopIndex;
@override
void initState() {
super.initState();
_angle = widget.angle;
_stops = List.generate(
widget.colors.length,
(i) => GradientStop(
color: widget.colors[i],
position: i < widget.stops.length
? widget.stops[i]
: i / (widget.colors.length - 1).clamp(1, 100),
),
);
if (_stops.isEmpty) {
_stops = [
const GradientStop(color: Color(0xFF6C63FF), position: 0.0),
const GradientStop(color: Color(0xFF4ECDC4), position: 1.0),
];
}
_selectedStopIndex = 0;
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFFF2F2F7),
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildHandle(),
const SizedBox(height: 12),
_buildHeader(),
const SizedBox(height: 16),
_buildPreview(),
const SizedBox(height: 16),
_buildGradientBar(),
const SizedBox(height: 16),
_buildAngleControl(),
const SizedBox(height: 16),
if (_selectedStopIndex != null) _buildColorEditor(),
const SizedBox(height: 16),
_buildActionButtons(),
SizedBox(height: MediaQuery.paddingOf(context).bottom + 8),
],
),
);
}
Widget _buildHandle() {
return Center(
child: Container(
width: 36,
height: 5,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(3),
),
),
);
}
Widget _buildHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () => Navigator.pop(context),
child: const Text(
'取消',
style: TextStyle(fontSize: 17, color: CupertinoColors.systemBlue),
),
),
const Text(
'🎨 渐变编辑器',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Color(0xFF1A1A2E),
),
),
CupertinoButton(
padding: EdgeInsets.zero,
onPressed: () {
_notifyChanged();
widget.onDone?.call();
Navigator.pop(context);
},
child: const Text(
'完成',
style: TextStyle(
fontSize: 17,
color: CupertinoColors.systemBlue,
fontWeight: FontWeight.w600,
),
),
),
],
);
}
Widget _buildPreview() {
return Container(
height: 120,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: _buildGradient(),
border: Border.all(color: Colors.white.withValues(alpha: 0.5)),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 12),
],
),
);
}
LinearGradient _buildGradient() {
final rad = _angle * pi / 180;
final begin = Alignment(sin(rad), -cos(rad));
final end = Alignment(-sin(rad), cos(rad));
final sortedStops = List<GradientStop>.from(_stops)
..sort((a, b) => a.position.compareTo(b.position));
return LinearGradient(
begin: begin,
end: end,
colors: sortedStops.map((s) => s.color).toList(),
stops: sortedStops.map((s) => s.position).toList(),
);
}
Widget _buildGradientBar() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'色标节点',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.black.withValues(alpha: 0.6),
),
),
const SizedBox(height: 8),
GestureDetector(
onHorizontalDragUpdate: (details) {
if (_selectedStopIndex == null) return;
final box = context.findRenderObject() as RenderBox;
final barWidth = box.size.width - 32;
final pos = (details.localPosition.dx - 16) / barWidth * 100 / 100;
final clamped = pos.clamp(0.0, 1.0);
setState(() {
_stops[_selectedStopIndex!] = _stops[_selectedStopIndex!]
.copyWith(position: clamped);
});
_notifyChanged();
},
child: Container(
height: 48,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: _buildGradient(),
border: Border.all(color: Colors.black.withValues(alpha: 0.1)),
),
child: Stack(
children: _stops.asMap().entries.map((entry) {
final index = entry.key;
final stop = entry.value;
final isSelected = index == _selectedStopIndex;
return Positioned(
left:
stop.position * (MediaQuery.of(context).size.width - 64) +
16 -
12,
top: isSelected ? 4 : 10,
child: GestureDetector(
onTap: () {
setState(() => _selectedStopIndex = index);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 24,
height: isSelected ? 40 : 28,
decoration: BoxDecoration(
color: stop.color,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: isSelected
? LightColors.primary
: Colors.white,
width: isSelected ? 2.5 : 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 4,
),
],
),
),
),
);
}).toList(),
),
),
),
],
);
}
Widget _buildAngleControl() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'角度 ${_angle.round()}°',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.black.withValues(alpha: 0.6),
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: CupertinoSlider(
value: _angle,
max: 360,
onChanged: (v) {
setState(() => _angle = v);
widget.onAngleChanged(v);
},
),
),
SizedBox(
width: 44,
height: 44,
child: CustomPaint(
painter: _AngleIndicatorPainter(angle: _angle),
),
),
],
),
],
);
}
Widget _buildColorEditor() {
final stop = _stops[_selectedStopIndex!];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'节点颜色',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.black.withValues(alpha: 0.6),
),
),
const SizedBox(height: 8),
EditorColorPicker(
currentColor: stop.color,
onChanged: (Color color) {
setState(() {
_stops[_selectedStopIndex!] = _stops[_selectedStopIndex!]
.copyWith(color: color);
});
_notifyChanged();
},
compact: true,
),
],
);
}
Widget _buildActionButtons() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: CupertinoColors.systemBlue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
onPressed: _addStop,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.plus,
size: 16,
color: CupertinoColors.systemBlue,
),
SizedBox(width: 4),
Text(
'添加节点',
style: TextStyle(
fontSize: 14,
color: CupertinoColors.systemBlue,
),
),
],
),
),
if (_stops.length > 2)
CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: CupertinoColors.systemRed.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
onPressed: _removeSelectedStop,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.minus,
size: 16,
color: CupertinoColors.systemRed,
),
SizedBox(width: 4),
Text(
'删除节点',
style: TextStyle(
fontSize: 14,
color: CupertinoColors.systemRed,
),
),
],
),
),
],
);
}
void _addStop() {
const newPos = 0.5;
final leftStop = _stops.where((s) => s.position < newPos).lastOrNull;
final rightStop = _stops.where((s) => s.position > newPos).firstOrNull;
Color newColor;
if (leftStop != null && rightStop != null) {
newColor =
Color.lerp(leftStop.color, rightStop.color, 0.5) ??
const Color(0xFF888888);
} else {
newColor = const Color(0xFF888888);
}
setState(() {
_stops.add(GradientStop(color: newColor, position: newPos));
_selectedStopIndex = _stops.length - 1;
});
_notifyChanged();
}
void _removeSelectedStop() {
if (_selectedStopIndex == null || _stops.length <= 2) return;
setState(() {
_stops.removeAt(_selectedStopIndex!);
_selectedStopIndex = _selectedStopIndex! >= _stops.length
? _stops.length - 1
: _selectedStopIndex;
});
_notifyChanged();
}
void _notifyChanged() {
widget.onChanged(_stops);
widget.onAngleChanged(_angle);
}
}
class _AngleIndicatorPainter extends CustomPainter {
const _AngleIndicatorPainter({required this.angle});
final double angle;
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 4;
final circlePaint = Paint()
..color = const Color(0xFFE5E5EA)
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawCircle(center, radius, circlePaint);
final rad = angle * pi / 180;
final endX = center.dx + radius * sin(rad);
final endY = center.dy - radius * cos(rad);
final linePaint = Paint()
..color = LightColors.primary
..strokeWidth = 2.5
..strokeCap = StrokeCap.round;
canvas.drawLine(center, Offset(endX, endY), linePaint);
final dotPaint = Paint()..color = LightColors.primary;
canvas.drawCircle(Offset(endX, endY), 3, dotPaint);
}
@override
bool shouldRepaint(covariant _AngleIndicatorPainter oldDelegate) {
return angle != oldDelegate.angle;
}
}