在 Flutter 开发中,滑块(Slider)是价格筛选、音量调节、评分设置、参数调整的高频场景(如电商价格区间筛选、音频音量控制、商品评分筛选、亮度调节)。原生Slider和RangeSlider存在样式固化、不支持自定义渐变轨道、刻度显示不灵活、标签格式化能力弱等问题,重复开发易导致交互体验不一致。本文封装的CommonSliderWidget整合 "单值滑块 + 范围滑块 + 刻度标记 + 自定义标签 + 全样式自定义" 五大核心能力,支持渐变轨道、最小间隔限制、深色模式适配,一行代码集成,覆盖 95%+ 滑块使用场景,彻底解决重复编码痛点!
一、核心优势(精准解决开发痛点)
✅ 单值 / 范围自由切换:通过isRange参数一键切换单值(如音量调节)和范围滑块(如价格区间),复用通用样式逻辑✅ 样式全自定义:轨道高度 / 颜色、滑块(thumb)大小 / 颜色 / 边框、刻度线 / 标签样式均可配置,支持渐变激活轨道✅ 刻度与标签增强:内置刻度标记(可开关)、自定义标签格式化(如价格 ¥XX、评分 X 分),标签跟随滑块实时显示✅ 交互体验优化:范围滑块内置最小间隔限制、滑动结束防抖回调、禁用状态样式适配、滑块阴影提升视觉层次✅ 高扩展性:支持自定义滑块样式(图片 / 图标)、标签显示策略、深色模式自动适配,兼容多业务场景✅ 鲁棒性强:内置参数校验(最小值 < 最大值、最小间隔非负),避免运行时异常
二、核心配置速览(关键参数一目了然)
| 配置分类 | 核心参数 | 核心作用 |
|---|---|---|
| 必选配置 | value/rangeValue |
单值滑块当前值 / 范围滑块当前区间(必填,与isRange匹配) |
| 必选配置 | onChanged/onRangeChanged |
单值变化回调 / 范围变化回调(必填,返回实时值 / 区间) |
| 功能配置 | isRange、min、max |
是否为范围滑块(默认 false)、最小值(默认 0)、最大值(默认 100) |
| 功能配置 | divisions、minRange |
刻度数量(默认 null,无刻度)、范围滑块最小间隔(默认 0,避免区间过窄) |
| 功能配置 | labelBuilder |
标签文本构建器(自定义格式化,如价格 ¥XX、评分 X 分) |
| 样式配置 | trackHeight、activeColor |
轨道高度(默认 4px)、激活轨道颜色(默认蓝色) |
| 样式配置 | inactiveColor、activeGradient |
未激活轨道颜色(默认浅灰)、激活轨道渐变(优先级高于activeColor) |
| 样式配置 | thumbSize、thumbColor |
滑块大小(默认 24px)、滑块背景色(默认白色) |
| 样式配置 | thumbBorderColor/Width |
滑块边框色(默认激活色)、边框宽度(默认 2px) |
| 样式配置 | showLabels、showDividers |
是否显示标签(默认 true)、是否显示刻度线(默认 true) |
| 样式配置 | labelStyle、dividerColor |
标签文本样式(默认 12 号黑灰色)、刻度线颜色(默认浅灰) |
| 扩展配置 | enabled、adaptDarkMode |
是否启用滑块(默认 true,禁用时灰化)、是否适配深色模式(默认 true) |
| 回调配置 | onChangeEnd/onRangeChangeEnd |
滑动结束回调(防抖,适合触发接口请求等耗时操作) |
三、生产级完整代码(可直接复制,开箱即用)
dart
import 'package:flutter/material.dart';
/// 自定义轨道形状(支持渐变背景)
class GradientSliderTrackShape extends RoundedRectSliderTrackShape {
final Gradient? gradient; // 轨道渐变
final double trackHeight; // 轨道高度
const GradientSliderTrackShape({
this.gradient,
required this.trackHeight,
});
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = true,
bool isDiscrete = false,
}) {
final double trackWidth = parentBox.size.width - sliderTheme.thumbShape!.getPreferredSize(true, isDiscrete).width;
final double trackLeft = offset.dx + (parentBox.size.width - trackWidth) / 2;
final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
bool isDiscrete = false,
bool isEnabled = true,
double additionalActiveTrackHeight = 2,
}) {
super.paint(
context,
offset,
parentBox: parentBox,
sliderTheme: sliderTheme,
enableAnimation: enableAnimation,
textDirection: textDirection,
thumbCenter: thumbCenter,
isDiscrete: isDiscrete,
isEnabled: isEnabled,
additionalActiveTrackHeight: 0,
);
// 绘制渐变激活轨道(覆盖默认纯色)
if (gradient != null) {
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final Rect activeRect = Rect.fromLTRB(
trackRect.left,
trackRect.top,
thumbCenter.dx,
trackRect.bottom,
);
final Paint paint = Paint()
..shader = gradient!.createShader(activeRect)
..style = PaintingStyle.fill;
context.canvas.drawRRect(
RRect.fromRectAndRadius(activeRect, Radius.circular(trackHeight / 2)),
paint,
);
}
}
}
/// 自定义范围滑块轨道形状(支持渐变)
class GradientRangeSliderTrackShape extends RoundedRectRangeSliderTrackShape {
final Gradient? gradient; // 轨道渐变
final double trackHeight; // 轨道高度
const GradientRangeSliderTrackShape({
this.gradient,
required this.trackHeight,
});
@override
Rect getPreferredRect({
required RenderBox parentBox,
Offset offset = Offset.zero,
required SliderThemeData sliderTheme,
bool isEnabled = true,
bool isDiscrete = false,
}) {
final double trackWidth = parentBox.size.width - sliderTheme.thumbShape!.getPreferredSize(true, isDiscrete).width * 2;
final double trackLeft = offset.dx + (parentBox.size.width - trackWidth) / 2;
final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
}
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset startThumbCenter,
required Offset endThumbCenter,
bool isDiscrete = false,
bool isEnabled = true,
double additionalActiveTrackHeight = 2,
}) {
super.paint(
context,
offset,
parentBox: parentBox,
sliderTheme: sliderTheme,
enableAnimation: enableAnimation,
textDirection: textDirection,
startThumbCenter: startThumbCenter,
endThumbCenter: endThumbCenter,
isDiscrete: isDiscrete,
isEnabled: isEnabled,
additionalActiveTrackHeight: 0,
);
// 绘制渐变激活轨道(覆盖默认纯色)
if (gradient != null) {
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
final Rect activeRect = Rect.fromLTRB(
startThumbCenter.dx,
trackRect.top,
endThumbCenter.dx,
trackRect.bottom,
);
final Paint paint = Paint()
..shader = gradient!.createShader(activeRect)
..style = PaintingStyle.fill;
context.canvas.drawRRect(
RRect.fromRectAndRadius(activeRect, Radius.circular(trackHeight / 2)),
paint,
);
}
}
}
/// 通用滑块组件(支持单值/范围选择、渐变轨道、自定义样式)
class CommonSliderWidget extends StatefulWidget {
// 单值滑块配置
final double? value;
final ValueChanged<double>? onChanged;
final ValueChanged<double>? onChangeEnd;
// 范围滑块配置
final RangeValues? rangeValue;
final ValueChanged<RangeValues>? onRangeChanged;
final ValueChanged<RangeValues>? onRangeChangeEnd;
// 通用功能配置
final bool isRange; // 是否为范围滑块(默认false)
final double min; // 最小值(默认0.0)
final double max; // 最大值(默认100.0)
final int? divisions; // 刻度数(默认null,无刻度)
final String Function(double)? labelBuilder; // 标签文本构建器
final double minRange; // 范围滑块最小间隔(默认0.0)
final bool enabled; // 是否启用(默认true)
// 样式配置
final double trackHeight; // 轨道高度(默认4.0)
final Color activeColor; // 激活轨道颜色(默认蓝色)
final Color inactiveColor; // 未激活轨道颜色(默认浅灰)
final Gradient? activeGradient; // 激活轨道渐变(优先级高于activeColor)
final double thumbSize; // 滑块大小(默认24.0)
final Color thumbColor; // 滑块背景色(默认白色)
final Color thumbBorderColor; // 滑块边框色(默认activeColor)
final double thumbBorderWidth; // 滑块边框宽度(默认2.0)
final bool showLabels; // 是否显示标签(默认true)
final bool showDividers; // 是否显示刻度线(默认true)
final TextStyle labelStyle; // 标签文本样式
final Color dividerColor; // 刻度线颜色(默认浅灰)
final double dividerHeight; // 刻度线高度(默认8.0)
// 适配配置
final bool adaptDarkMode; // 适配深色模式(默认true)
const CommonSliderWidget({
super.key,
// 单值配置
this.value,
this.onChanged,
this.onChangeEnd,
// 范围配置
this.rangeValue,
this.onRangeChanged,
this.onRangeChangeEnd,
// 通用功能配置
this.isRange = false,
this.min = 0.0,
this.max = 100.0,
this.divisions,
this.labelBuilder,
this.minRange = 0.0,
this.enabled = true,
// 样式配置
this.trackHeight = 4.0,
this.activeColor = Colors.blue,
this.inactiveColor = const Color(0xFFE0E0E0),
this.activeGradient,
this.thumbSize = 24.0,
this.thumbColor = Colors.white,
this.thumbBorderColor = Colors.blue,
this.thumbBorderWidth = 2.0,
this.showLabels = true,
this.showDividers = true,
this.labelStyle = const TextStyle(fontSize: 12, color: Colors.black87),
this.dividerColor = const Color(0xFFE0E0E0),
this.dividerHeight = 8.0,
// 适配配置
this.adaptDarkMode = true,
}) : assert(!isRange
? (value != null && onChanged != null)
: (rangeValue != null && onRangeChanged != null),
"单值滑块需配置value和onChanged,范围滑块需配置rangeValue和onRangeChanged"),
assert(min < max, "最小值不能大于等于最大值"),
assert(minRange >= 0.0, "最小间隔不能为负数"),
assert(minRange < max - min, "最小间隔需小于最大值与最小值的差值");
@override
State<CommonSliderWidget> createState() => _CommonSliderWidgetState();
}
class _CommonSliderWidgetState extends State<CommonSliderWidget> {
late double _currentValue; // 单值滑块当前值
late RangeValues _currentRangeValue; // 范围滑块当前值
@override
void initState() {
super.initState();
// 初始化当前值(外部传入值优先)
_currentValue = widget.value ?? widget.min;
_currentRangeValue = widget.rangeValue ?? RangeValues(widget.min, widget.max);
}
@override
void didUpdateWidget(covariant CommonSliderWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// 外部值变化时同步更新内部状态
if (!widget.isRange && widget.value != oldWidget.value) {
_currentValue = widget.value!;
}
if (widget.isRange && widget.rangeValue != oldWidget.rangeValue) {
_currentRangeValue = widget.rangeValue!;
}
}
/// 深色模式颜色适配
Color _adaptDarkMode(Color lightColor, Color darkColor) {
if (!widget.adaptDarkMode) return lightColor;
return MediaQuery.platformBrightnessOf(context) == Brightness.dark
? darkColor
: lightColor;
}
/// 构建标签文本(支持自定义格式化)
String _getLabelText(double value) {
if (widget.labelBuilder != null) {
return widget.labelBuilder!(value);
}
// 默认格式化:整数显示整数,小数保留1位
return value == value.round()
? value.round().toString()
: value.toStringAsFixed(1);
}
/// 构建滑块主题配置
SliderThemeData _buildSliderTheme() {
// 适配深色模式颜色
final adaptedActiveColor = _adaptDarkMode(widget.activeColor, Colors.blueAccent);
final adaptedInactiveColor = _adaptDarkMode(widget.inactiveColor, const Color(0xFF444444));
final adaptedDividerColor = _adaptDarkMode(widget.dividerColor, const Color(0xFF555555));
final adaptedLabelStyle = widget.labelStyle.copyWith(
color: _adaptDarkMode(
widget.labelStyle.color ?? Colors.black87,
Colors.white70,
),
);
final adaptedThumbBorderColor = _adaptDarkMode(widget.thumbBorderColor, Colors.blueAccent);
final adaptedThumbColor = _adaptDarkMode(widget.thumbColor, const Color(0xFF333333));
return SliderThemeData(
// 轨道配置
trackHeight: widget.trackHeight,
activeTrackColor: adaptedActiveColor,
inactiveTrackColor: adaptedInactiveColor,
// 滑块配置
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: widget.thumbSize / 2,
disabledThumbRadius: widget.thumbSize / 2,
),
thumbColor: adaptedThumbColor,
overlayColor: adaptedActiveColor.withOpacity(0.1),
overlayShape: RoundSliderOverlayShape(overlayRadius: widget.thumbSize / 2 + 2),
// 刻度配置
tickMarkShape: widget.showDividers
? RoundSliderTickMarkShape(tickMarkRadius: 1.5)
: const NoTickMarkShape(),
tickMarkColor: adaptedDividerColor,
tickMarkHeight: widget.dividerHeight,
// 标签配置
valueIndicatorColor: adaptedActiveColor,
valueIndicatorShape: const PaddleSliderValueIndicatorShape(),
valueIndicatorTextStyle: adaptedLabelStyle,
// 边框配置
thumbDecoration: BoxDecoration(
border: Border.all(
color: adaptedThumbBorderColor,
width: widget.thumbBorderWidth,
),
borderRadius: BorderRadius.circular(widget.thumbSize / 2),
boxShadow: const [
BoxShadow(color: Colors.black12, blurRadius: 4, offset: Offset(0, 2)),
],
),
);
}
/// 构建单值滑块
Widget _buildSingleSlider() {
final sliderTheme = _buildSliderTheme();
// 自定义渐变轨道
final trackShape = GradientSliderTrackShape(
gradient: widget.activeGradient,
trackHeight: widget.trackHeight,
);
return SliderTheme(
data: sliderTheme.copyWith(
trackShape: trackShape,
),
child: Slider(
value: _currentValue,
min: widget.min,
max: widget.max,
divisions: widget.divisions,
label: _getLabelText(_currentValue),
showValueIndicator: widget.showLabels
? ShowValueIndicator.always
: ShowValueIndicator.never,
onChanged: widget.enabled
? (value) {
setState(() => _currentValue = value);
widget.onChanged!(value);
}
: null,
onChangeEnd: widget.onChangeEnd,
activeColor: sliderTheme.activeTrackColor,
inactiveColor: sliderTheme.inactiveTrackColor,
thumbColor: sliderTheme.thumbColor,
enabled: widget.enabled,
),
);
}
/// 构建范围滑块
Widget _buildRangeSlider() {
final sliderTheme = _buildSliderTheme();
// 自定义渐变轨道
final trackShape = GradientRangeSliderTrackShape(
gradient: widget.activeGradient,
trackHeight: widget.trackHeight,
);
return SliderTheme(
data: sliderTheme.copyWith(
trackShape: trackShape,
),
child: RangeSlider(
values: _currentRangeValue,
min: widget.min,
max: widget.max,
divisions: widget.divisions,
labels: RangeLabels(
_getLabelText(_currentRangeValue.start),
_getLabelText(_currentRangeValue.end),
),
showValueIndicator: widget.showLabels
? ShowValueIndicator.always
: ShowValueIndicator.never,
onChanged: widget.enabled
? (values) {
// 限制最小间隔
if (values.end - values.start < widget.minRange) return;
setState(() => _currentRangeValue = values);
widget.onRangeChanged!(values);
}
: null,
onChangeEnd: widget.onRangeChangeEnd,
activeColor: sliderTheme.activeTrackColor,
inactiveColor: sliderTheme.inactiveTrackColor,
thumbColor: sliderTheme.thumbColor,
enabled: widget.enabled,
),
);
}
@override
Widget build(BuildContext context) {
// 外层容器:适配滑块点击区域
return Container(
height: widget.thumbSize + widget.dividerHeight + 20, // 预留标签显示空间
padding: const EdgeInsets.symmetric(horizontal: 8),
child: widget.isRange ? _buildRangeSlider() : _buildSingleSlider(),
);
}
}
/// 空刻度样式(隐藏刻度线)
class NoTickMarkShape extends SliderTickMarkShape {
const NoTickMarkShape();
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero;
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
bool isDiscrete = false,
bool isEnabled = true,
}) {}
}
四、四大高频场景实战示例(直接复制可用)
场景 1:电商价格筛选(范围滑块 + 渐变轨道)
适用场景:电商 APP 商品列表价格区间筛选,支持最小间隔限制、价格标签格式化
dart
class PriceRangeFilterPage extends StatefulWidget {
@override
State<PriceRangeFilterPage> createState() => _PriceRangeFilterPageState();
}
class _PriceRangeFilterPageState extends State<PriceRangeFilterPage> {
late RangeValues _priceRange; // 价格区间(0-2000元)
@override
void initState() {
super.initState();
_priceRange = const RangeValues(0, 1000); // 初始区间:0-1000元
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("价格筛选")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("价格区间", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const SizedBox(height: 16),
// 价格范围滑块
CommonSliderWidget(
isRange: true,
rangeValue: _priceRange,
onRangeChanged: (values) => setState(() => _priceRange = values),
onRangeChangeEnd: (values) {
debugPrint("筛选价格区间:${values.start.round()}-${values.end.round()}元");
// 实际业务:调用接口筛选该价格区间的商品
},
min: 0,
max: 2000,
divisions: 20, // 每100元一个刻度
minRange: 100, // 最小间隔100元(避免区间过窄)
// 样式配置
trackHeight: 6,
activeColor: Colors.orangeAccent,
activeGradient: const LinearGradient(
colors: [Colors.orange, Colors.redAccent],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
inactiveColor: const Color(0xFFF5F5F5),
thumbSize: 28,
thumbColor: Colors.white,
thumbBorderColor: Colors.orangeAccent,
thumbBorderWidth: 2,
showLabels: true,
showDividers: true,
// 价格标签格式化(¥XX)
labelBuilder: (value) => "¥${value.round()}",
labelStyle: const TextStyle(fontSize: 13, color: Colors.white),
dividerColor: Colors.orangeAccent.withOpacity(0.3),
adaptDarkMode: true,
),
const SizedBox(height: 24),
// 价格区间预览
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("¥${_priceRange.start.round()}", style: const TextStyle(fontSize: 16)),
const SizedBox(width: 16),
const Text("至", style: TextStyle(color: Colors.grey)),
const SizedBox(width: 16),
Text("¥${_priceRange.end.round()}", style: const TextStyle(fontSize: 16)),
],
),
const Spacer(),
// 确认筛选按钮
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orangeAccent,
),
onPressed: () => Navigator.pop(context),
child: const Text("确认筛选"),
),
),
],
),
),
);
}
}
场景 2:音频音量调节(单值滑块 + 隐藏标签 / 刻度)
适用场景:音频 / 视频 APP 音量调节,简洁样式,无标签 / 刻度,滑块颜色与音量关联
dart
class VolumeControlPage extends StatefulWidget {
@override
State<VolumeControlPage> createState() => _VolumeControlPageState();
}
class _VolumeControlPageState extends State<VolumeControlPage> {
double _volume = 60; // 初始音量60%
// 根据音量获取滑块激活色(低→中→高:红→黄→绿)
Color _getVolumeColor() {
if (_volume < 30) return Colors.redAccent;
if (_volume < 70) return Colors.yellowAccent;
return Colors.greenAccent;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("音量调节")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 音量滑块(带音量图标)
Row(
children: [
Icon(Icons.volume_down, color: _getVolumeColor()),
const SizedBox(width: 16),
Expanded(
child: CommonSliderWidget(
value: _volume,
onChanged: (value) => setState(() => _volume = value),
onChangeEnd: (value) => debugPrint("音量设置为:${value.round()}%"),
min: 0,
max: 100,
divisions: 10, // 每10%一个刻度(隐藏显示)
// 样式配置
trackHeight: 8,
activeColor: _getVolumeColor(),
inactiveColor: const Color(0xFFE0E0E0),
thumbSize: 24,
thumbColor: _getVolumeColor(),
thumbBorderColor: Colors.white,
thumbBorderWidth: 3,
showLabels: false, // 隐藏标签
showDividers: false, // 隐藏刻度线
adaptDarkMode: true,
),
),
const SizedBox(width: 16),
Icon(Icons.volume_up, color: _getVolumeColor()),
],
),
const SizedBox(height: 24),
// 当前音量预览
Center(
child: Text(
"当前音量:${_volume.round()}%",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: _getVolumeColor(),
),
),
),
],
),
),
);
}
}
场景 3:商品评分筛选(单值滑块 + 自定义标签)
适用场景:电商 APP 商品列表评分筛选,0-5 分刻度,标签显示 "X 分",配合星级展示
dart
class ProductRatingPage extends StatefulWidget {
@override
State<ProductRatingPage> createState() => _ProductRatingPageState();
}
class _ProductRatingPageState extends State<ProductRatingPage> {
double _rating = 4.0; // 初始最低评分4分
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("筛选商品评分")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("最低评分", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const SizedBox(height: 16),
// 评分滑块
CommonSliderWidget(
value: _rating,
onChanged: (value) => setState(() => _rating = value),
onChangeEnd: (value) {
debugPrint("筛选评分≥${value}的商品");
// 实际业务:调用接口筛选评分≥value的商品
},
min: 0,
max: 5,
divisions: 5, // 0-5分,每1分一个刻度
// 样式配置
trackHeight: 6,
activeColor: Colors.yellowAccent,
activeGradient: const LinearGradient(
colors: [Colors.yellow, Colors.orange],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
inactiveColor: const Color(0xFFF5F5F5),
thumbSize: 26,
thumbColor: Colors.white,
thumbBorderColor: Colors.yellowAccent,
showLabels: true,
showDividers: true,
// 评分标签格式化(X分)
labelBuilder: (value) => "${value}分",
labelStyle: const TextStyle(fontSize: 13, color: Colors.white),
dividerColor: Colors.yellowAccent.withOpacity(0.3),
adaptDarkMode: true,
),
const SizedBox(height: 24),
// 星级预览
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
final starIndex = index + 1;
return Icon(
starIndex <= _rating ? Icons.star : Icons.star_border,
color: Colors.yellowAccent,
size: 24,
);
}),
),
),
],
),
),
);
}
}
场景 4:屏幕亮度调节(单值滑块 + 禁用状态)
适用场景:APP 设置页屏幕亮度调节,支持禁用状态(如省电模式下),深色模式适配
dart
class BrightnessControlPage extends StatefulWidget {
@override
State<BrightnessControlPage> createState() => _BrightnessControlPageState();
}
class _BrightnessControlPageState extends State<BrightnessControlPage> {
double _brightness = 80; // 初始亮度80%
bool _isPowerSaving = false; // 是否开启省电模式(禁用滑块)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("屏幕亮度")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 省电模式开关
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("省电模式", style: TextStyle(fontSize: 16)),
Switch(
value: _isPowerSaving,
onChanged: (value) => setState(() => _isPowerSaving = value),
activeColor: Colors.blueAccent,
),
],
),
const SizedBox(height: 16),
// 亮度滑块(省电模式下禁用)
CommonSliderWidget(
value: _brightness,
onChanged: (value) => setState(() => _brightness = value),
onChangeEnd: (value) {
debugPrint("亮度设置为:${value.round()}%");
// 实际业务:调用原生API设置屏幕亮度
},
min: 0,
max: 100,
divisions: 10,
enabled: !_isPowerSaving, // 省电模式禁用滑块
// 样式配置
trackHeight: 6,
activeColor: Colors.blueAccent,
inactiveColor: const Color(0xFFE0E0E0),
thumbSize: 24,
thumbColor: Colors.white,
thumbBorderColor: Colors.blueAccent,
showLabels: true,
showDividers: true,
labelBuilder: (value) => "${value.round()}%",
adaptDarkMode: true,
),
const SizedBox(height: 24),
// 亮度预览
Center(
child: Container(
width: 100,
height: 60,
decoration: BoxDecoration(
color: Colors.black.withOpacity((100 - _brightness) / 100),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[200]!),
),
child: Center(
child: Text(
"亮度预览",
style: TextStyle(
color: Colors.white.withOpacity(_brightness / 100),
),
),
),
),
),
],
),
),
);
}
}
五、核心封装技巧(复用成熟设计思路)
- 单值 / 范围统一封装 :通过
isRange参数切换滑块类型,复用_buildSliderTheme统一样式配置,减少代码冗余;通过断言校验参数一致性,避免运行时异常。 - 渐变轨道实现 :自定义
GradientSliderTrackShape和GradientRangeSliderTrackShape,重写paint方法绘制渐变激活轨道,优先级高于纯色,满足个性化设计需求。 - 标签灵活格式化 :提供
labelBuilder回调,支持自定义标签文本(如价格 ¥XX、评分 X 分),适配不同业务场景的标签展示需求。 - 交互体验优化 :
- 范围滑块内置
minRange限制,避免选中区间过窄(如价格区间 < 100 元); - 滑块添加阴影和边框,提升视觉层次;
- 禁用状态自动灰化,交互逻辑更清晰。
- 范围滑块内置
- 深色模式适配 :通过
_adaptDarkMode方法统一处理所有可视化颜色,自动识别系统主题,无需单独配置深色模式样式。 - 样式隔离 :通过
SliderTheme隔离组件样式,避免影响全局滑块主题,同时保证组件内部样式统一。
六、避坑指南(解决 90% 开发痛点)
- 参数一致性校验 :单值滑块必须配置
value和onChanged,范围滑块必须配置rangeValue和onRangeChanged,否则触发断言错误;初始化时需确保min < max。 - 最小间隔限制 :范围滑块
minRange需小于max - min(如 max=2000、min=0 时,minRange<2000),否则无法正常滑动,组件已内置断言校验。 - 刻度数适配 :
divisions需满足(max - min) % divisions == 0(如 0-2000 元,divisions=20→每 100 元一个刻度),否则刻度分布不均匀。 - 深色模式兼容 :自定义颜色(如轨道色、滑块色)必须通过
_adaptDarkMode方法适配,避免深色模式下白色文本配白色背景导致不可见。 - 标签显示优化 :标签文本需精简(如 "¥1000" 而非 "1000 元"),避免标签重叠;通过
showValueIndicator控制标签显示时机(always/never/onlyForDiscrete)。 - 滑块点击区域:外层容器需预留足够高度(包含滑块 + 标签 + 刻度),避免标签被截断;建议高度 = 滑块大小 + 刻度高度 + 20。
- 性能优化 :滑动过程中避免在
onChanged中执行耗时操作(如接口请求),建议在onChangeEnd中执行,减少性能损耗。
欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。