Flutter for OpenHarmony Web开发助手App实战:CSS生成器
写CSS样式的时候,经常需要调整各种属性来达到想要的效果。今天我们来实现一个CSS生成器,让你可以通过可视化的方式生成CSS代码。
写CSS样式的时候,经常需要调整各种属性来达到想要的效果。今天我们来实现一个CSS生成器,让你可以通过可视化的方式生成CSS代码。
功能定位
CSS生成器的核心价值是降低CSS编写的门槛。不是每个人都能记住所有CSS属性,通过可视化界面调整参数,实时看到代码和效果,这种体验要好得多。
我们重点实现几个常用的CSS功能:边框、阴影、渐变、圆角。这些是最常用也最容易出错的属性。
完整代码实现
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class CssGeneratorPage extends StatefulWidget {
const CssGeneratorPage({Key? key}) : super(key: key);
State<CssGeneratorPage> createState() => _CssGeneratorPageState();
}
class _CssGeneratorPageState extends State<CssGeneratorPage> {
// 边框属性
double _borderWidth = 2.0;
Color _borderColor = Colors.blue;
double _borderRadius = 8.0;
// 阴影属性
double _shadowX = 0.0;
double _shadowY = 4.0;
double _shadowBlur = 8.0;
Color _shadowColor = Colors.black26;
// 背景属性
Color _backgroundColor = Colors.white;
// 内边距
double _padding = 16.0;
int _selectedTab = 0;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('CSS生成器'),
actions: [
IconButton(
icon: const Icon(Icons.copy),
onPressed: _copyCssCode,
tooltip: '复制CSS代码',
),
],
),
body: Column(
children: [
// 标签切换
Container(
color: Colors.grey[100],
child: Row(
children: [
_buildTab('边框', 0),
_buildTab('阴影', 1),
_buildTab('背景', 2),
_buildTab('间距', 3),
],
),
),
// 控制面板
Expanded(
child: Row(
children: [
// 左侧参数调整区
Expanded(
flex: 2,
child: Container(
color: Colors.grey[50],
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: _buildControlPanel(),
),
),
),
// 右侧预览和代码区
Expanded(
flex: 3,
child: Container(
color: Colors.white,
child: Column(
children: [
// 预览区
Expanded(
flex: 2,
child: Container(
padding: EdgeInsets.all(24.w),
child: Center(
child: _buildPreview(),
),
),
),
Divider(height: 1.h),
// 代码区
Expanded(
flex: 1,
child: Container(
color: Colors.grey[900],
padding: EdgeInsets.all(16.w),
child: SingleChildScrollView(
child: SelectableText(
_generateCssCode(),
style: TextStyle(
fontFamily: 'monospace',
fontSize: 13.sp,
color: Colors.greenAccent,
),
),
),
),
),
],
),
),
),
],
),
),
],
),
);
}
Widget _buildTab(String title, int index) {
final isSelected = _selectedTab == index;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _selectedTab = index),
child: Container(
padding: EdgeInsets.symmetric(vertical: 12.h),
decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.transparent,
border: Border(
bottom: BorderSide(
color: isSelected ? Colors.blue : Colors.transparent,
width: 2.w,
),
),
),
child: Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.blue : Colors.grey[600],
),
),
),
),
);
}
Widget _buildControlPanel() {
switch (_selectedTab) {
case 0:
return _buildBorderControls();
case 1:
return _buildShadowControls();
case 2:
return _buildBackgroundControls();
case 3:
return _buildPaddingControls();
default:
return Container();
}
}
Widget _buildBorderControls() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('边框宽度', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
Slider(
value: _borderWidth,
min: 0,
max: 10,
divisions: 20,
label: '${_borderWidth.toStringAsFixed(1)}px',
onChanged: (value) => setState(() => _borderWidth = value),
),
SizedBox(height: 16.h),
Text('边框颜色', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 8.h),
Wrap(
spacing: 8.w,
children: [
Colors.blue,
Colors.red,
Colors.green,
Colors.orange,
Colors.purple,
Colors.black,
].map((color) => _buildColorButton(color, _borderColor, (c) {
setState(() => _borderColor = c);
})).toList(),
),
SizedBox(height: 16.h),
Text('圆角半径', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
Slider(
value: _borderRadius,
min: 0,
max: 50,
divisions: 50,
label: '${_borderRadius.toStringAsFixed(0)}px',
onChanged: (value) => setState(() => _borderRadius = value),
),
],
);
}
Widget _buildShadowControls() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('水平偏移', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
Slider(
value: _shadowX,
min: -20,
max: 20,
divisions: 40,
label: '${_shadowX.toStringAsFixed(0)}px',
onChanged: (value) => setState(() => _shadowX = value),
),
SizedBox(height: 16.h),
Text('垂直偏移', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
Slider(
value: _shadowY,
min: -20,
max: 20,
divisions: 40,
label: '${_shadowY.toStringAsFixed(0)}px',
onChanged: (value) => setState(() => _shadowY = value),
),
SizedBox(height: 16.h),
Text('模糊半径', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
Slider(
value: _shadowBlur,
min: 0,
max: 30,
divisions: 30,
label: '${_shadowBlur.toStringAsFixed(0)}px',
onChanged: (value) => setState(() => _shadowBlur = value),
),
SizedBox(height: 16.h),
Text('阴影颜色', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 8.h),
Wrap(
spacing: 8.w,
children: [
Colors.black26,
Colors.black54,
Colors.blue.withOpacity(0.3),
Colors.red.withOpacity(0.3),
].map((color) => _buildColorButton(color, _shadowColor, (c) {
setState(() => _shadowColor = c);
})).toList(),
),
],
);
}
Widget _buildBackgroundControls() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('背景颜色', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 8.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: [
Colors.white,
Colors.grey[100]!,
Colors.blue[50]!,
Colors.green[50]!,
Colors.orange[50]!,
Colors.purple[50]!,
].map((color) => _buildColorButton(color, _backgroundColor, (c) {
setState(() => _backgroundColor = c);
})).toList(),
),
],
);
}
Widget _buildPaddingControls() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('内边距', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
Slider(
value: _padding,
min: 0,
max: 50,
divisions: 50,
label: '${_padding.toStringAsFixed(0)}px',
onChanged: (value) => setState(() => _padding = value),
),
],
);
}
Widget _buildColorButton(Color color, Color currentColor, Function(Color) onTap) {
final isSelected = color == currentColor;
return GestureDetector(
onTap: () => onTap(color),
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: color,
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey[300]!,
width: isSelected ? 3.w : 1.w,
),
borderRadius: BorderRadius.circular(8.r),
),
),
);
}
Widget _buildPreview() {
return Container(
width: 200.w,
height: 150.h,
padding: EdgeInsets.all(_padding),
decoration: BoxDecoration(
color: _backgroundColor,
border: Border.all(
color: _borderColor,
width: _borderWidth,
),
borderRadius: BorderRadius.circular(_borderRadius),
boxShadow: [
BoxShadow(
color: _shadowColor,
offset: Offset(_shadowX, _shadowY),
blurRadius: _shadowBlur,
),
],
),
child: Center(
child: Text(
'预览效果',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
),
);
}
String _generateCssCode() {
final buffer = StringBuffer();
buffer.writeln('.element {');
// 边框
if (_borderWidth > 0) {
buffer.writeln(' border: ${_borderWidth.toStringAsFixed(1)}px solid ${_colorToHex(_borderColor)};');
}
// 圆角
if (_borderRadius > 0) {
buffer.writeln(' border-radius: ${_borderRadius.toStringAsFixed(0)}px;');
}
// 背景
buffer.writeln(' background-color: ${_colorToHex(_backgroundColor)};');
// 内边距
if (_padding > 0) {
buffer.writeln(' padding: ${_padding.toStringAsFixed(0)}px;');
}
// 阴影
if (_shadowBlur > 0 || _shadowX != 0 || _shadowY != 0) {
buffer.writeln(' box-shadow: ${_shadowX.toStringAsFixed(0)}px ${_shadowY.toStringAsFixed(0)}px ${_shadowBlur.toStringAsFixed(0)}px ${_colorToHex(_shadowColor)};');
}
buffer.writeln('}');
return buffer.toString();
}
String _colorToHex(Color color) {
return '#${color.value.toRadixString(16).substring(2, 8)}';
}
void _copyCssCode() {
Clipboard.setData(ClipboardData(text: _generateCssCode()));
Get.snackbar(
'复制成功',
'CSS代码已复制到剪贴板',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2),
);
}
}
界面布局设计
整个页面分为三个主要区域:
顶部标签栏:切换不同的CSS属性类别。
左侧控制面板:调整具体的CSS参数。
右侧预览区:上半部分显示实时效果,下半部分显示生成的CSS代码。
这种布局让用户可以边调整边看效果,非常直观。
标签切换实现
使用一个简单的索引变量控制显示哪个面板:
int _selectedTab = 0;
Widget _buildTab(String title, int index) {
final isSelected = _selectedTab == index;
return GestureDetector(
onTap: () => setState(() => _selectedTab = index),
child: Container(
decoration: BoxDecoration(
color: isSelected ? Colors.white : Colors.transparent,
border: Border(
bottom: BorderSide(
color: isSelected ? Colors.blue : Colors.transparent,
width: 2.w,
),
),
),
child: Text(title),
),
);
}
选中的标签会有蓝色下划线和白色背景,视觉反馈很清晰。
滑块控件使用
Slider是调整数值的最佳选择,直观又好用:
Slider(
value: _borderWidth,
min: 0,
max: 10,
divisions: 20,
label: '${_borderWidth.toStringAsFixed(1)}px',
onChanged: (value) => setState(() => _borderWidth = value),
)
divisions参数:把滑块分成20个刻度,让调整更精确。
label参数:显示当前值,用户知道自己在调什么。
onChanged回调:实时更新状态,预览立即刷新。
颜色选择器实现
我们用一组色块来实现颜色选择:
Widget _buildColorButton(Color color, Color currentColor, Function(Color) onTap) {
final isSelected = color == currentColor;
return GestureDetector(
onTap: () => onTap(color),
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: color,
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey[300]!,
width: isSelected ? 3.w : 1.w,
),
borderRadius: BorderRadius.circular(8.r),
),
),
);
}
选中的颜色会有更粗的蓝色边框,一眼就能看出来。
虽然颜色选项有限,但覆盖了最常用的几种。如果要支持自定义颜色,可以集成flutter_colorpicker包。
实时预览实现
预览区直接使用Flutter的Container来模拟CSS效果:
Container(
decoration: BoxDecoration(
color: _backgroundColor,
border: Border.all(
color: _borderColor,
width: _borderWidth,
),
borderRadius: BorderRadius.circular(_borderRadius),
boxShadow: [
BoxShadow(
color: _shadowColor,
offset: Offset(_shadowX, _shadowY),
blurRadius: _shadowBlur,
),
],
),
)
Flutter的BoxDecoration和CSS的样式属性几乎是一一对应的,所以预览效果非常准确。
CSS代码生成
根据当前的参数值,动态生成CSS代码:
String _generateCssCode() {
final buffer = StringBuffer();
buffer.writeln('.element {');
if (_borderWidth > 0) {
buffer.writeln(' border: ${_borderWidth.toStringAsFixed(1)}px solid ${_colorToHex(_borderColor)};');
}
if (_borderRadius > 0) {
buffer.writeln(' border-radius: ${_borderRadius.toStringAsFixed(0)}px;');
}
buffer.writeln('}');
return buffer.toString();
}
使用StringBuffer拼接字符串,效率比直接用加号高。
只有当属性值不为默认值时才输出,避免生成冗余代码。
颜色转换工具
Flutter的Color对象需要转换成CSS的十六进制格式:
String _colorToHex(Color color) {
return '#${color.value.toRadixString(16).substring(2, 8)}';
}
color.value是一个32位整数,包含ARGB四个通道。我们只需要RGB部分,所以截取后6位。
复制功能实现
点击复制按钮,把CSS代码复制到剪贴板:
void _copyCssCode() {
Clipboard.setData(ClipboardData(text: _generateCssCode()));
Get.snackbar(
'复制成功',
'CSS代码已复制到剪贴板',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2),
);
}
使用GetX的snackbar给用户一个反馈,体验更好。
性能优化
每次调整参数都会触发setState,重建整个页面。这在参数不多的情况下没问题,但如果功能扩展了,可能会卡顿。
优化方法是把不同的区域拆分成独立的StatefulWidget,只重建需要更新的部分。
class BorderControlPanel extends StatefulWidget {
final Function(double, Color, double) onChanged;
// ...
}
这样调整边框参数时,阴影控制面板就不会重建了。
功能扩展建议
更多CSS属性:文字样式、定位、变换等。
预设模板:提供一些常用的样式组合,用户可以快速选择。
历史记录:保存用户生成过的CSS,方便复用。
导出功能:除了复制,还可以导出为CSS文件。
响应式预览:模拟不同屏幕尺寸下的效果。
实战经验
做这个功能时,最大的挑战是如何让界面简洁又功能完整。CSS属性太多了,全部放上去会很乱。
最后决定只做最常用的几个属性,通过标签切换来组织。这样既保持了界面清爽,又能满足大部分需求。
还有一个细节:滑块的范围设置很重要。比如边框宽度,设置0-10px就够了,设置0-100px反而不好用,因为大部分时候只需要1-5px。
小结
CSS生成器通过可视化的方式降低了CSS编写的难度。用户不需要记住所有属性名和语法,只需要拖动滑块、选择颜色,就能生成标准的CSS代码。
核心技术点包括:标签切换、滑块控件、实时预览、代码生成。把这些组合起来,就是一个实用的CSS工具。
记住:工具的价值在于降低使用门槛,让复杂的事情变简单。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)