用 Flutter CupertinoPicker 封装一个带单位的『身高/体重』选择器(附完整代码)
·
Flutter CupertinoPicker 实战:打造带单位的身高体重选择器
在健康管理类应用中,身高体重选择器是用户信息录入的核心组件之一。传统的数字选择器往往无法直观展示单位(如"cm"或"kg"),而iOS风格的CupertinoPicker原生也不支持单位显示。本文将带你从零实现一个高度定制化的选择器组件,解决以下实际问题:
- 如何在不修改原生组件的前提下实现数值+单位的联动显示
- 处理高频滚动事件导致的性能问题
- 实现精确的视觉对齐和触摸区域控制
- 构建可复用的业务组件库
1. 需求分析与技术选型
1.1 业务场景拆解
在健康档案录入界面,用户需要选择:
- 身高范围:100-250cm,步长1cm
- 体重范围:30-200kg,步长0.5kg
设计稿要求:
- 数值居左,单位居右
- 滑动时数值和单位同步高亮
- 支持默认值设置
- 滑动结束才触发回调(防抖)
1.2 CupertinoPicker 的局限性
原生组件存在三个主要问题:
- 无法直接添加静态文本(如单位)
onSelectedItemChanged会实时触发(无防抖)- 选中项样式定制困难
// 原生组件的基本用法
CupertinoPicker(
itemExtent: 40,
children: List.generate(10, (i) => Text('$i')),
onSelectedItemChanged: (index) {
print('实时触发: $index'); // 需要防抖处理
},
)
1.3 技术方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 重写滚动控件 | 完全可控 | 开发成本高,易出兼容问题 |
| 组合现有组件 | 快速实现,维护成本低 | 需要处理布局细节 |
| 使用第三方库 | 开箱即用 | 定制灵活性差 |
我们选择 组合方案 :通过 Stack 叠加CupertinoPicker和单位文本,用 Positioned 精确定位。
2. 核心实现步骤
2.1 组件结构设计
class UnitPicker extends StatelessWidget {
final List<int> values;
final String unit;
final ValueChanged<int> onChanged;
const UnitPicker({...});
@override
Widget build(BuildContext context) {
return Stack(
children: [
CupertinoPicker(...), // 数值选择器
Positioned(...), // 单位文本
],
);
}
}
2.2 单位定位技巧
关键点在于计算单位的垂直位置:
- 选择器高度:200
- 每项高度:40
- 单位Y坐标 = (总高度/2) - (项高度/2)
Positioned(
left: 100, // 根据实际文本宽度调整
top: (200/2) - (40/2), // 垂直居中
child: Text('cm', style: TextStyle(...)),
)
2.3 防抖处理实现
原生选择器在滚动时会频繁触发回调,需要通过Timer实现防抖:
Timer? _debounceTimer;
void _handleChange(int index) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
widget.onChanged(values[index]);
});
}
3. 完整组件代码
以下是可直接复用的 HeightWeightPicker 组件:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class HeightWeightPicker extends StatefulWidget {
final int minValue;
final int maxValue;
final double step;
final String unit;
final int initialValue;
final ValueChanged<int> onChanged;
const HeightWeightPicker({
Key? key,
required this.minValue,
required this.maxValue,
this.step = 1,
required this.unit,
required this.initialValue,
required this.onChanged,
}) : super(key: key);
@override
_HeightWeightPickerState createState() => _HeightWeightPickerState();
}
class _HeightWeightPickerState extends State<HeightWeightPicker> {
late FixedExtentScrollController _controller;
late List<int> _values;
Timer? _debounceTimer;
@override
void initState() {
super.initState();
_values = _generateValues();
_controller = FixedExtentScrollController(
initialItem: _values.indexOf(widget.initialValue),
);
}
List<int> _generateValues() {
final values = <int>[];
for (var i = widget.minValue; i <= widget.maxValue; i += widget.step) {
values.add(i);
}
return values;
}
void _handleChange(int index) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
widget.onChanged(_values[index]);
});
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
child: Stack(
alignment: Alignment.center,
children: [
CupertinoPicker(
scrollController: _controller,
itemExtent: 40,
onSelectedItemChanged: _handleChange,
children: _values.map((value) {
return Center(
child: Text(
value.toString(),
style: const TextStyle(fontSize: 24),
),
);
}).toList(),
),
Positioned(
left: 80,
child: Text(
widget.unit,
style: const TextStyle(
fontSize: 18,
color: CupertinoColors.label,
),
),
),
],
),
);
}
@override
void dispose() {
_controller.dispose();
_debounceTimer?.cancel();
super.dispose();
}
}
4. 业务场景应用
4.1 身高选择器配置
HeightWeightPicker(
minValue: 100,
maxValue: 250,
step: 1,
unit: 'cm',
initialValue: 170,
onChanged: (value) {
print('选择的身高: $value cm');
},
)
4.2 体重选择器配置
HeightWeightPicker(
minValue: 30,
maxValue: 200,
step: 0.5,
unit: 'kg',
initialValue: 65,
onChanged: (value) {
print('选择的体重: $value kg');
},
)
4.3 表单集成示例
Column(
children: [
const Text('身高(cm)'),
HeightWeightPicker(...), // 身高选择器
const SizedBox(height: 20),
const Text('体重(kg)'),
HeightWeightPicker(...), // 体重选择器
ElevatedButton(
onPressed: () => _submitForm(),
child: const Text('保存'),
),
],
)
5. 高级优化技巧
5.1 性能优化方案
对于大范围数值(如100-250):
- 使用
ListView.builder替代全量生成 - 实现视窗缓存提高滚动流畅度
ListView.builder(
controller: _controller,
itemExtent: 40,
itemCount: _values.length,
itemBuilder: (context, index) {
return Text(_values[index].toString());
},
)
5.2 视觉增强技巧
- 添加选中项高亮效果:
BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey[300]!),
bottom: BorderSide(color: Colors.grey[300]!),
),
)
- 使用
CupertinoTheme统一风格:
CupertinoTheme(
data: CupertinoThemeData(
textTheme: CupertinoTextThemeData(
pickerTextStyle: TextStyle(...),
),
),
child: CupertinoPicker(...),
)
5.3 多语言适配方案
通过扩展支持动态单位:
class IntlUnitPicker extends HeightWeightPicker {
final String Function(BuildContext) unitBuilder;
@override
Widget build(BuildContext context) {
final unit = unitBuilder(context);
return super.build(context);
}
}
使用时:
IntlUnitPicker(
unitBuilder: (context) => Localizations.of<AppLocalizations>(context)!.heightUnit,
...
)
6. 避坑指南
6.1 常见问题排查
-
单位文本不显示
- 检查
Stack的尺寸约束 - 确认
Positioned的坐标计算
- 检查
-
回调触发过于频繁
- 调整防抖时长(300-500ms为宜)
- 确保Timer在dispose时被取消
-
初始值不生效
- 确认initialValue在min/max范围内
- 检查scrollController初始化逻辑
6.2 手势冲突解决
当单位区域需要响应点击时:
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => _controller.jumpToItem(100),
child: Text(widget.unit),
)
6.3 跨平台适配建议
Android平台可添加Material风格后备:
Theme.of(context).platform == TargetPlatform.android
? MaterialPicker(...)
: CupertinoPicker(...)
更多推荐
所有评论(0)