Files
xianyan/lib/editor/widgets/panels/canvas_style_sheet.dart
Developer b11d53ca58 feat: 新增画布样式后处理功能及导出一致性优化
refactor(editor): 将画布样式相关功能移至独立文件
fix(editor): 修复描边坐标偏移问题
feat(export): 新增applyCanvasStyle后处理方法
feat(settings): 实现CanvasStyleModel持久化存储
fix(feed): 互动操作增加登录检查避免401错误
feat(wallpaper): 实现无限下拉加载功能
fix(text): 修复富文本编辑器初始化问题
2026-05-05 08:24:28 +08:00

1172 lines
34 KiB
Dart

// ============================================================
// 闲言APP — 画布样式编辑面板
// 创建时间: 2026-05-05
// 更新时间: 2026-05-05
// 作用: 画布边缘样式编辑 — 圆角+边框+阴影+叠层效果
// 独立文件,后续扩展开发
// 上次更新: 初始创建 — 4大编辑区域+实时预览
// ============================================================
import 'package:flex_color_picker/flex_color_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'
show Colors, showModalBottomSheet, DraggableScrollableSheet;
import 'package:xianyan/core/theme/app_theme.dart';
import 'package:pro_image_editor/core/models/canvas_style_model.dart';
class CanvasStyleSheet extends StatefulWidget {
const CanvasStyleSheet({
super.key,
required this.style,
required this.onChanged,
});
final CanvasStyleModel style;
final ValueChanged<CanvasStyleModel> onChanged;
@override
State<CanvasStyleSheet> createState() => _CanvasStyleSheetState();
static void show({
required BuildContext context,
required CanvasStyleModel style,
required ValueChanged<CanvasStyleModel> onChanged,
}) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => DraggableScrollableSheet(
initialChildSize: 0.75,
minChildSize: 0.5,
maxChildSize: 0.9,
expand: false,
builder: (context, scrollController) => Container(
decoration: BoxDecoration(
color: AppTheme.ext(context).bgPrimary,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: CanvasStyleSheet(style: style, onChanged: onChanged),
),
),
);
}
}
class _CanvasStyleSheetState extends State<CanvasStyleSheet> {
late CanvasStyleModel _style;
@override
void initState() {
super.initState();
_style = widget.style;
}
void _update(CanvasStyleModel newStyle) {
setState(() => _style = newStyle);
widget.onChanged(newStyle);
}
@override
Widget build(BuildContext context) {
final ext = AppTheme.ext(context);
return Column(
children: [
_buildHandle(ext),
_buildHeader(ext),
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
const SizedBox(height: 8),
_buildPreview(ext),
const SizedBox(height: 16),
_buildRadiusSection(ext),
const SizedBox(height: 20),
_buildBorderSection(ext),
const SizedBox(height: 20),
_buildShadowSection(ext),
const SizedBox(height: 20),
_buildStackSection(ext),
const SizedBox(height: 20),
_buildOuterMarginSection(ext),
const SizedBox(height: 40),
],
),
),
],
);
}
Widget _buildHandle(AppThemeExtension ext) {
return Center(
child: Container(
width: 40,
height: 5,
margin: const EdgeInsets.only(top: 10, bottom: 4),
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(3),
),
),
);
}
Widget _buildHeader(AppThemeExtension ext) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: Row(
children: [
Icon(CupertinoIcons.rectangle, size: 20, color: ext.accent),
const SizedBox(width: 8),
Text(
'画布样式',
style: TextStyle(
color: ext.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
const Spacer(),
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: ext.bgCard.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
CupertinoIcons.xmark,
color: ext.textSecondary,
size: 14,
),
),
),
],
),
);
}
// ─── 实时预览 ───
Widget _buildPreview(AppThemeExtension ext) {
return Container(
height: 160,
alignment: Alignment.center,
child: _CanvasPreviewCard(style: _style, ext: ext),
);
}
// ─── 圆角 ───
Widget _buildRadiusSection(AppThemeExtension ext) {
return _SectionCard(
ext: ext,
icon: CupertinoIcons.rectangle,
title: '圆角',
value: '${_style.borderRadius.round()}px',
child: Column(
children: [
Row(
children: [
_RadiusPreset(
ext: ext,
label: '直角',
radius: 0,
current: _style.borderRadius,
onTap: () => _update(_style.copyWith(borderRadius: 0)),
),
const SizedBox(width: 6),
_RadiusPreset(
ext: ext,
label: '',
radius: 8,
current: _style.borderRadius,
onTap: () => _update(_style.copyWith(borderRadius: 8)),
),
const SizedBox(width: 6),
_RadiusPreset(
ext: ext,
label: '',
radius: 20,
current: _style.borderRadius,
onTap: () => _update(_style.copyWith(borderRadius: 20)),
),
const SizedBox(width: 6),
_RadiusPreset(
ext: ext,
label: '',
radius: 40,
current: _style.borderRadius,
onTap: () => _update(_style.copyWith(borderRadius: 40)),
),
const SizedBox(width: 6),
_RadiusPreset(
ext: ext,
label: '超大',
radius: 60,
current: _style.borderRadius,
onTap: () => _update(_style.copyWith(borderRadius: 60)),
),
],
),
const SizedBox(height: 8),
CupertinoSlider(
value: _style.borderRadius,
min: 0,
max: 80,
divisions: 40,
activeColor: ext.accent,
onChanged: (v) => _update(_style.copyWith(borderRadius: v)),
),
],
),
);
}
// ─── 边框 ───
Widget _buildBorderSection(AppThemeExtension ext) {
return _SectionCard(
ext: ext,
icon: CupertinoIcons.square,
title: '边框',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: CanvasBorderStyle.values.map((s) {
final isActive = _style.borderStyle == s;
return Expanded(
child: GestureDetector(
onTap: () => _update(_style.copyWith(borderStyle: s)),
child: _PresetChip(
ext: ext,
label: s.label,
isActive: isActive,
),
),
);
}).toList(),
),
if (_style.borderStyle != CanvasBorderStyle.none) ...[
const SizedBox(height: 12),
_buildSliderRow(
ext,
'粗细',
_style.borderWidth,
0.5,
8.0,
15,
(v) => _update(_style.copyWith(borderWidth: v)),
suffix: '${_style.borderWidth.toStringAsFixed(1)}px',
),
const SizedBox(height: 10),
_buildColorRow(
ext,
'颜色',
_style.borderColor,
(c) => _update(_style.copyWith(borderColor: c)),
),
],
],
),
);
}
// ─── 阴影 ───
Widget _buildShadowSection(AppThemeExtension ext) {
return _SectionCard(
ext: ext,
icon: CupertinoIcons.circle_lefthalf_fill,
title: '阴影',
child: Column(
children: [
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () =>
_update(_style.copyWith(shadowBlur: 0, shadowOpacity: 0)),
child: _PresetChip(
ext: ext,
label: '',
isActive: !_style.hasShadow,
),
),
),
const SizedBox(width: 6),
Expanded(
child: GestureDetector(
onTap: () => _update(
_style.copyWith(shadowBlur: 8, shadowOpacity: 0.2),
),
child: _PresetChip(
ext: ext,
label: '',
isActive: _style.hasShadow && _style.shadowOpacity < 0.4,
),
),
),
const SizedBox(width: 6),
Expanded(
child: GestureDetector(
onTap: () => _update(
_style.copyWith(shadowBlur: 16, shadowOpacity: 0.45),
),
child: _PresetChip(
ext: ext,
label: '',
isActive: _style.hasShadow && _style.shadowOpacity >= 0.4,
),
),
),
],
),
if (_style.hasShadow) ...[
const SizedBox(height: 12),
_buildSliderRow(
ext,
'模糊',
_style.shadowBlur,
5,
50,
45,
(v) => _update(_style.copyWith(shadowBlur: v)),
suffix: '${_style.shadowBlur.round()}px',
),
const SizedBox(height: 8),
_buildSliderRow(
ext,
'浓度',
_style.shadowOpacity,
0.05,
1.0,
19,
(v) => _update(_style.copyWith(shadowOpacity: v)),
suffix: '${(_style.shadowOpacity * 100).round()}%',
),
const SizedBox(height: 8),
_buildSliderRow(
ext,
'扩散',
_style.shadowSpread,
0,
30,
30,
(v) => _update(_style.copyWith(shadowSpread: v)),
suffix: '${_style.shadowSpread.round()}px',
),
const SizedBox(height: 8),
_buildSliderRow(
ext,
'↔ 偏移',
_style.shadowOffsetX.abs(),
0,
30,
30,
(v) => _update(
_style.copyWith(
shadowOffsetX: _style.shadowOffsetX < 0 ? -v : v,
),
),
suffix: '${_style.shadowOffsetX.round()}px',
),
const SizedBox(height: 8),
_buildSliderRow(
ext,
'↕ 偏移',
_style.shadowOffsetY.abs(),
0,
30,
30,
(v) => _update(
_style.copyWith(
shadowOffsetY: _style.shadowOffsetY < 0 ? -v : v,
),
),
suffix: '${_style.shadowOffsetY.round()}px',
),
const SizedBox(height: 8),
Row(
children: [
_buildOffsetDirectionButton(
ext,
icon: CupertinoIcons.arrow_left,
label: '',
onTap: () => _update(
_style.copyWith(
shadowOffsetX: (_style.shadowOffsetX - 2).clamp(
-30.0,
30.0,
),
),
),
),
const SizedBox(width: 4),
_buildOffsetDirectionButton(
ext,
icon: CupertinoIcons.arrow_right,
label: '',
onTap: () => _update(
_style.copyWith(
shadowOffsetX: (_style.shadowOffsetX + 2).clamp(
-30.0,
30.0,
),
),
),
),
const SizedBox(width: 4),
_buildOffsetDirectionButton(
ext,
icon: CupertinoIcons.arrow_up,
label: '',
onTap: () => _update(
_style.copyWith(
shadowOffsetY: (_style.shadowOffsetY - 2).clamp(
-30.0,
30.0,
),
),
),
),
const SizedBox(width: 4),
_buildOffsetDirectionButton(
ext,
icon: CupertinoIcons.arrow_down,
label: '',
onTap: () => _update(
_style.copyWith(
shadowOffsetY: (_style.shadowOffsetY + 2).clamp(
-30.0,
30.0,
),
),
),
),
const SizedBox(width: 4),
_buildOffsetDirectionButton(
ext,
icon: CupertinoIcons.arrow_uturn_left,
label: '重置',
onTap: () => _update(
_style.copyWith(shadowOffsetX: 0, shadowOffsetY: 0),
),
),
],
),
],
],
),
);
}
// ─── 叠层 ───
Widget _buildStackSection(AppThemeExtension ext) {
return _SectionCard(
ext: ext,
icon: CupertinoIcons.square_stack_3d_down_right,
title: '叠层',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSliderRow(
ext,
'层数',
_style.stackLayers.toDouble(),
0,
5,
5,
(v) => _update(_style.copyWith(stackLayers: v.round())),
suffix: '${_style.stackLayers}',
),
if (_style.hasStack) ...[
const SizedBox(height: 10),
_buildSliderRow(
ext,
'间距',
_style.stackDistance,
2,
30,
14,
(v) => _update(_style.copyWith(stackDistance: v)),
suffix: '${_style.stackDistance.round()}px',
),
const SizedBox(height: 12),
Text(
'位置',
style: TextStyle(
color: ext.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Row(
children: CanvasStackPosition.values.map((p) {
final isActive = _style.stackPosition == p;
return Expanded(
child: GestureDetector(
onTap: () => _update(_style.copyWith(stackPosition: p)),
child: _PresetChip(
ext: ext,
label: p.label,
isActive: isActive,
),
),
);
}).toList(),
),
],
],
),
);
}
// ─── 外边距 ───
Widget _buildOuterMarginSection(AppThemeExtension ext) {
return _SectionCard(
ext: ext,
icon: CupertinoIcons.resize,
title: '外边距',
// subtitle: '控制画布与最外层视图的距离',
child: Column(
children: [
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => _update(_style.copyWith(outerMargin: -1.0)),
child: _PresetChip(
ext: ext,
label: '自动',
isActive: !_style.hasOuterMargin,
),
),
),
const SizedBox(width: 6),
Expanded(
child: GestureDetector(
onTap: () => _update(_style.copyWith(outerMargin: 0.0)),
child: _PresetChip(
ext: ext,
label: '紧贴',
isActive: _style.outerMargin == 0,
),
),
),
const SizedBox(width: 6),
Expanded(
child: GestureDetector(
onTap: () => _update(_style.copyWith(outerMargin: 10.0)),
child: _PresetChip(
ext: ext,
label: '适中',
isActive:
_style.outerMargin > 0 && _style.outerMargin <= 15,
),
),
),
const SizedBox(width: 6),
Expanded(
child: GestureDetector(
onTap: () => _update(_style.copyWith(outerMargin: 30.0)),
child: _PresetChip(
ext: ext,
label: '宽松',
isActive: _style.outerMargin > 15,
),
),
),
],
),
if (_style.hasOuterMargin) ...[
const SizedBox(height: 12),
_buildSliderRow(
ext,
'边距',
_style.outerMargin,
0,
60,
60,
(v) => _update(_style.copyWith(outerMargin: v)),
suffix: '${_style.outerMargin.round()}px',
),
],
],
),
);
}
// ─── 通用组件 ───
Widget _buildSliderRow(
AppThemeExtension ext,
String label,
double value,
double min,
double max,
int divisions,
ValueChanged<double> onChanged, {
String suffix = '',
}) {
return Row(
children: [
SizedBox(
width: 36,
child: Text(
label,
style: TextStyle(color: ext.textSecondary, fontSize: 12),
),
),
Expanded(
child: CupertinoSlider(
value: value.clamp(min, max),
min: min,
max: max,
divisions: divisions,
activeColor: ext.accent,
onChanged: onChanged,
),
),
SizedBox(
width: 48,
child: Text(
suffix,
style: TextStyle(
color: ext.accent,
fontSize: 12,
fontWeight: FontWeight.w600,
fontFeatures: const [FontFeature.tabularFigures()],
),
textAlign: TextAlign.right,
),
),
],
);
}
Widget _buildOffsetDirectionButton(
AppThemeExtension ext, {
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: ext.bgCard.withValues(alpha: 0.3),
width: 0.5,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: ext.textSecondary),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(fontSize: 10, color: ext.textSecondary),
),
],
),
),
),
);
}
Widget _buildColorRow(
AppThemeExtension ext,
String label,
Color color,
ValueChanged<Color> onChanged,
) {
return Row(
children: [
SizedBox(
width: 36,
child: Text(
label,
style: TextStyle(color: ext.textSecondary, fontSize: 12),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () => _showColorPicker(ext, color, onChanged),
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: ext.textHint.withValues(alpha: 0.3),
width: 0.5,
),
),
),
),
const SizedBox(width: 8),
Text(
'#${color.toARGB32().toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}',
style: TextStyle(
color: ext.textSecondary,
fontSize: 11,
fontFeatures: const [FontFeature.tabularFigures()],
),
),
],
);
}
void _showColorPicker(
AppThemeExtension ext,
Color current,
ValueChanged<Color> onChanged,
) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (sheetContext) => Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
decoration: BoxDecoration(
color: ext.bgPrimary,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
Text(
'选择颜色',
style: TextStyle(
color: ext.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
CupertinoButton(
padding: EdgeInsets.zero,
minSize: 32,
onPressed: () => Navigator.pop(sheetContext),
child: Text(
'完成',
style: TextStyle(color: ext.accent, fontSize: 14),
),
),
],
),
),
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
child: ColorPicker(
color: current,
onColorChanged: onChanged,
width: 36,
height: 36,
borderRadius: 8,
spacing: 6,
runSpacing: 6,
wheelDiameter: 200,
showMaterialName: false,
showColorName: false,
showColorCode: true,
colorCodeHasColor: true,
pickersEnabled: const {
ColorPickerType.wheel: true,
ColorPickerType.accent: false,
ColorPickerType.primary: false,
ColorPickerType.both: false,
},
copyPasteBehavior: const ColorPickerCopyPasteBehavior(
copyButton: false,
pasteButton: false,
),
),
),
),
],
),
),
);
}
}
// ============================================================
// 区域卡片
// ============================================================
class _SectionCard extends StatelessWidget {
const _SectionCard({
required this.ext,
required this.icon,
required this.title,
this.value,
required this.child,
});
final AppThemeExtension ext;
final IconData icon;
final String title;
final String? value;
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: ext.bgSecondary.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: ext.bgCard.withValues(alpha: 0.3),
width: 0.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 16, color: ext.accent),
const SizedBox(width: 6),
Text(
title,
style: TextStyle(
color: ext.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
if (value != null) ...[
const Spacer(),
Text(
value!,
style: TextStyle(
color: ext.accent,
fontSize: 13,
fontWeight: FontWeight.w600,
fontFeatures: const [FontFeature.tabularFigures()],
),
),
],
],
),
const SizedBox(height: 10),
child,
],
),
);
}
}
// ============================================================
// 预设芯片
// ============================================================
class _PresetChip extends StatelessWidget {
const _PresetChip({
required this.ext,
required this.label,
required this.isActive,
});
final AppThemeExtension ext;
final String label;
final bool isActive;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: BoxDecoration(
color: isActive ? ext.accent.withValues(alpha: 0.12) : ext.bgSecondary,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isActive
? ext.accent.withValues(alpha: 0.4)
: ext.bgCard.withValues(alpha: 0.3),
width: 0.5,
),
),
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
color: isActive ? ext.accent : ext.textSecondary,
),
),
),
);
}
}
// ============================================================
// 圆角预设
// ============================================================
class _RadiusPreset extends StatelessWidget {
const _RadiusPreset({
required this.ext,
required this.label,
required this.radius,
required this.current,
required this.onTap,
});
final AppThemeExtension ext;
final String label;
final double radius;
final double current;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final isActive = (current - radius).abs() < 2;
return Expanded(
child: GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
decoration: BoxDecoration(
color: isActive
? ext.accent.withValues(alpha: 0.12)
: ext.bgSecondary,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isActive
? ext.accent.withValues(alpha: 0.4)
: ext.bgCard.withValues(alpha: 0.3),
width: 0.5,
),
),
child: Column(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: ext.bgCard,
borderRadius: BorderRadius.circular(radius.clamp(0, 12)),
border: Border.all(
color: isActive ? ext.accent : ext.textHint,
width: 1,
),
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 9,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
color: isActive ? ext.accent : ext.textSecondary,
),
),
],
),
),
),
);
}
}
// ============================================================
// 实时预览卡片
// ============================================================
class _CanvasPreviewCard extends StatelessWidget {
const _CanvasPreviewCard({required this.style, required this.ext});
final CanvasStyleModel style;
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
final stackWidgets = <Widget>[];
if (style.hasStack) {
for (int i = style.stackLayers; i >= 1; i--) {
stackWidgets.add(_buildStackLayer(i));
}
}
stackWidgets.add(_buildMainCard());
return Stack(
alignment: style.stackPosition.alignment,
clipBehavior: Clip.none,
children: stackWidgets,
);
}
Widget _buildStackLayer(int index) {
final offset = style.stackDistance * index;
final dx = style.stackPosition == CanvasStackPosition.left
? -offset
: style.stackPosition == CanvasStackPosition.right
? offset
: 0.0;
final dy = style.stackPosition == CanvasStackPosition.top
? -offset
: style.stackPosition == CanvasStackPosition.bottom
? offset
: 0.0;
return Transform.translate(
offset: Offset(dx, dy),
child: Container(
width: 120,
height: 80,
decoration: BoxDecoration(
color: ext.bgCard.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(style.borderRadius.clamp(0, 40)),
border: style.hasBorder
? Border.all(
color: style.borderColor.withValues(alpha: 0.2),
width: style.borderWidth * 0.5,
)
: null,
),
),
);
}
Widget _buildMainCard() {
final radius = style.borderRadius.clamp(0.0, 40.0).toDouble();
Widget card = Container(
width: 120,
height: 80,
decoration: BoxDecoration(
color: ext.bgElevated,
borderRadius: BorderRadius.circular(radius),
boxShadow: style.hasShadow
? [
BoxShadow(
color: Colors.black.withValues(alpha: style.shadowOpacity),
blurRadius: (style.shadowBlur * 0.3).toDouble(),
spreadRadius: (style.shadowSpread * 0.3).toDouble(),
offset: Offset(
(style.shadowOffsetX * 0.3).toDouble(),
(style.shadowOffsetY != 0
? style.shadowOffsetY * 0.3
: style.shadowBlur * 0.08)
.toDouble(),
),
),
]
: null,
),
child: Center(
child: Text(
'预览',
style: TextStyle(color: ext.textSecondary, fontSize: 12),
),
),
);
if (style.hasBorder) {
if (style.borderStyle == CanvasBorderStyle.dashed) {
card = CustomPaint(
painter: _PreviewDashedPainter(
borderColor: style.borderColor,
borderWidth: style.borderWidth,
borderRadius: radius,
),
child: card,
);
} else {
card = DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(radius),
border: Border.all(
color: style.borderColor,
width: style.borderWidth,
),
),
child: card,
);
}
}
return card;
}
}
class _PreviewDashedPainter extends CustomPainter {
_PreviewDashedPainter({
required this.borderColor,
required this.borderWidth,
required this.borderRadius,
});
final Color borderColor;
final double borderWidth;
final double borderRadius;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = borderColor
..strokeWidth = borderWidth
..style = PaintingStyle.stroke;
final half = borderWidth / 2;
final rrect = RRect.fromRectAndRadius(
Rect.fromLTWH(
half,
half,
size.width - borderWidth,
size.height - borderWidth,
),
Radius.circular(borderRadius),
);
const dashWidth = 6.0;
const dashGap = 4.0;
final path = Path()..addRRect(rrect);
final metrics = path.computeMetrics();
for (final metric in metrics) {
double distance = 0;
while (distance < metric.length) {
final end = (distance + dashWidth).clamp(0.0, metric.length).toDouble();
canvas.drawPath(metric.extractPath(distance, end), paint);
distance += dashWidth + dashGap;
}
}
}
@override
bool shouldRepaint(covariant _PreviewDashedPainter old) =>
old.borderColor != borderColor ||
old.borderWidth != borderWidth ||
old.borderRadius != borderRadius;
}