Flutter 自定义数据选择器
前言
Flutter 项目中需要实现一个类似地址的选择器, 网上看了一些博客都是些不能满足需求的,在pub.flutter-io.cn/ 中搜索了picker插件,均不能满足需求,自己手撸一个。
环境
macOS 10.15.7
Flutter 2.0.6
Xcode Version 12.4
vscode
实现一个picker
针对 dart List类型 和json数据 都能实现,json格式的数据实现放在下一篇 json转dart中去介绍,请关注。
需要安装的依赖:
flutter_custom_dialog 用它来做dialog
dependencies:
flutter_custom_dialog: ^1.2.0
传入picker中的数据格式:
[
{
"value": "xxx",
"label": "xxx",
"children": [
{
"value": "xxxx",
"label": "xxx"
}
]
},
……
]
如果key值对应不上,需要重新组装key,demo中有相关的逻辑。
picker核心代码
custom_picker.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_custom_dialog/flutter_custom_dialog.dart';
/**
* params 里面目前支持参数
* indexs 指多个列的数组下标,如果是初始的时候可以传空数组 [],代码会自动处理成每个下标为0
* colNum 指多少列
* initData 传入的初始数据
*/
YYDialog customPicker(BuildContext context, params, Function onConfirm) {
return YYDialog().build(context)
..gravity = Gravity.bottom
..gravityAnimationEnable = true
..backgroundColor = Colors.transparent
..widget(ChooseList(params: params, onConfirm: onConfirm,))
..show();
}
class ChooseList extends StatefulWidget {
final Function onConfirm;
final params;
const ChooseList({Key ?key, this.params, required this.onConfirm,}) : super(key: key);
@override
_ChooseListState createState() => _ChooseListState();
}
class _ChooseListState extends State<ChooseList> {
List<dynamic> colIndex=[]; //数组下标集合
List<dynamic> colContentList = []; //所有列实时的数据源
final List<FixedExtentScrollController> scrollController = [];
@override
void initState() {
super.initState();
final indexs = widget.params['indexs'];
final colNum = widget.params['colNum'];
if (scrollController.length == 0) {
if(indexs.length == 0) { //没选择数据的时候是这个逻辑
for (int i = 0; i < colNum; i++) { // colNum 表示多少列
scrollController.add(FixedExtentScrollController(initialItem: 0));
}
} else { // 选择完数据的时候
for (int i = 0; i < colNum; i++) {
scrollController.add(FixedExtentScrollController(initialItem: indexs[i]));
}
}
}
initIndexs(indexs, colNum);
colContentList = initData(colIndex, colNum, widget.params['initData']);
}
//初始化下标
void initIndexs(indexs,colNum) {
if(indexs.length ==0){
for (int i = 0; i < colNum; i++) {
colIndex.add(0);
}
} else { // 选中之后再次进来
colIndex = indexs;
}
}
/**
* industriesData 数据源
*
* */
initData(indexs, colNum, initData) {
var dataList = []; //最终的各列的数据
var level = 0;
for(var i = 0; i < colNum; i++) {
dataList.add([]);
}
recursionDataHandle(indexs, colNum, initData, level,dataList);
return dataList;
}
/**
* 递归执行函数
*/
void recursionDataHandle(indexs, colNum, initData, level,dataList) {
for(var i = 0; i < initData.length; i++) {
if(level != colNum) {
dataList[level].add({"value": initData[i]['value'], "label": initData[i]['label']});
} else { //已经执行n层
return ;
}
}
//处理下一级的数据
var levelData = initData?[indexs[level]];
if(levelData !=null && levelData['children'] != null && levelData['children'].length>0) { //递归
level++; //层级加1
recursionDataHandle(indexs, colNum, initData[indexs[level-1]]['children'], level, dataList);
}
}
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: 280,
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular(16)),
color: Colors.white,
),
child: Column(
children: [
vGap(10),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center, //横轴居中对齐(默认)
children: [
GestureDetector( //手势
onTap: () {
Navigator.pop(context);
},
child: Container(
width: 30.0,
height: 30.0,
child: Text(
"取消",
style: TextStyle(
color: Colors.grey,
fontSize: 14),
)
),
),
Text(
"",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600),
),
GestureDetector(
onTap: () {
Navigator.pop(context);
var names = getAllNames();
widget.onConfirm({'indexs':colIndex, "names":names });
},
child: Container(
width: 30.0,
height: 30.0,
child: Text(
"确定",
style: TextStyle(
color: Colors.blueAccent,
fontSize: 14),
)
),
)
],
),
),
vGap(10),
Row(
children: cuputedScroll()
)
],
),
);
}
Widget buildCity(
{List<dynamic> ? list,
FixedExtentScrollController ? scroll,
int ? columnNum,
Function ? onSelected}) {
return Expanded(
flex: 1,
child: Container(
height: 230,
child: list?.length != 0
? CupertinoPicker.builder(
scrollController: scroll,
itemExtent: 30,
diameterRatio: 3,
squeeze: 0.8,
onSelectedItemChanged: (int _index) {
selectdeHandel(_index, columnNum);
},
itemBuilder: (context, index) {
return Center(
child: Text(
"${list?[index]['label']}",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600),
));
},
childCount: list?.length,
)
: Container()),
);
}
// 纵向间距
static SizedBox vGap(double height){
return SizedBox(
height: height,
);
}
// 计算 scroll的列
cuputedScroll () {
List<Widget> buildCitys =[];
for(var i =0; i< colContentList.length; i++ ){
buildCitys.add(buildCity(list: colContentList[i], scroll: scrollController[i],columnNum: i));
}
return buildCitys;
}
// 滑动某一列的列的时候
selectdeHandel(int _index, columnNum) {
for(var i = columnNum; i< colIndex.length; i++) {
setState(() {
if(i== columnNum) {
colIndex[i] = _index;
} else {
colIndex[i] = 0;
}
});
}
var tmpData = initData(colIndex, widget.params['colNum'], widget.params['initData']);
setState(() {
colContentList = tmpData;
});
if(columnNum !=colIndex.length-1) { //不是最后一列,滚动前一列,则后面每一列都需要滚动第一个元素位置上
if (scrollController[columnNum+1].hasClients) {
scrollController[columnNum+1].jumpTo(0.0);
}
}
}
//获取选择的数据
getAllNames() {
List<dynamic> names = [];
for(var i = 0; i < colContentList.length; i++) {
if(colContentList[i].length >0) {
names.add(colContentList[i][colIndex[i]]);
}
}
return names;
}
}
如何调用
custom-pick-page.dart
import 'package:flutter/material.dart';
import 'package:personal_app/components/custom-picker.dart';
import 'package:personal_app/data/industries-data.dart';
import 'package:personal_app/data/area-data.dart';
class CustomPickerPage extends StatefulWidget {
@override
_CustomPickerPageState createState() => _CustomPickerPageState();
}
class _CustomPickerPageState extends State<CustomPickerPage> {
List<dynamic> areaDataIdxs = [];//省市的选择数据下标集合
String areaName = '';// 省市名称
dynamic areaValue; //省市选的数据id
List<dynamic> provAreaDataIdxs = [];//省市区的选择数据下标集合
String provAreaName = '';// 省市区名称
dynamic provAreaValue; //省市区选的数据id
String industryName=''; //行业名称
List<dynamic> industryIdxs = [];//选择行业的下标集合
dynamic industryValue; //行业选的数据id
List<dynamic> resetAreaData = []; //省市区数据转换后的数据
@override
void initState() {
//需要注意的是 areaData 数据字段不是label 和value; 需要转化一下
resetAreaData =recursionDataHandle(areaData);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("自定义数据选择器"),
),
body: Center(
child: GestureDetector(
onTap: (){
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
RaisedButton(
padding: EdgeInsets.all(0),
onPressed: () {
customPicker(context,
{"indexs":industryIdxs, "initData": industriesData, "colNum":2},
(opt) {
setState(() {
industryIdxs = opt['indexs'];
List names = opt['names'];
industryName = '';
for(int i = 0; i< names.length; i++) {
industryName += names[i]['label'] != '' ? i== 0 ? names[i]['label'] : '/' + names[i]['label'] : '';
}
industryValue = names[names.length-1]['value'];//value 这里逻辑只需要取最后一个
});
// vm.setIndustryIdxs(industryIdxs);
});
},
child: Row(
children: [
Text('行业选择选择'),
],
),
),
Text(industryName),
Container(
height: 18.0,
),
SizedBox(height:8.0),
RaisedButton(
padding: EdgeInsets.all(0),
onPressed: () {
customPicker(context,
{"indexs":areaDataIdxs, "initData": resetAreaData, "colNum":2},
(opt) {
setState(() {
areaDataIdxs = opt['indexs'];
List names = opt['names'];
areaName = '';
for(int i = 0; i< names.length; i++) {
areaName += names[i]['label'] != '' ? i== 0 ? names[i]['label'] : '/' + names[i]['label'] : '';
}
areaValue = names[names.length-1]['value'];//value 这里逻辑只需要取最后一个
});
});
},
child: Row(
children: [
Text('省市选择(需要重新组装字段)'),
],
),
),
Text(areaName),
Container(
height: 26.0,
),
RaisedButton(
padding: EdgeInsets.all(0),
onPressed: () {
customPicker(context,
{"indexs":provAreaDataIdxs, "initData": resetAreaData, "colNum":3},
(opt) {
setState(() {
provAreaDataIdxs = opt['indexs'];
List names = opt['names'];
provAreaName = '';
for(int i = 0; i< names.length; i++) {
provAreaName += names[i]['label'] != '' ? i== 0 ? names[i]['label'] : '/' + names[i]['label'] : '';
}
provAreaValue = names[names.length-1]['value'];//value 这里逻辑只需要取最后一个
});
});
},
child: Row(
children: [
Text('省市区选择(需要重新组装字段)'),
],
),
),
Text(provAreaName),
Container(
height: 26.0,
),
]
),
),
),
);
}
//数据格式转换
recursionDataHandle(data) {
List<Map<String, dynamic>> resetData = [];
if(data?.length > 0) {
for (var i = 0; i <data?.length; i++) {
resetData.add({
'value': data[i]['id'],
'label': data[i]['name'],
'center': data[i]['center'],
'level': data[i]['level'],
// 'children': data[i]['children'] ? recursionDataHandle(data[i]['children']): []
});
if(data[i].containsKey('children')) { //是否包含key值children
if(data[i]['children']?.length > 0) {
resetData[i]['children'] = recursionDataHandle(data[i]['children']);
} else {
resetData[i]['children'] = [];
}
}
}
}
return resetData;
}
}
industries-data.dart 和 area-data.dart 是数据文件,这个不再贴出来,有需要的下面会给出github地址查看
industries-data.dart 行业数据
area-data.dart 省市区数据
areaData 需要重新组装数据 id-> value name->label
效果:
1.选中数据再次点击选择器数据回显。 2.可以动态显示多列 3.多列显示实现数据的联动
项目源码 github.com/chenbing11/… master分支 有需要请自取
结语
创作不易,请多点赞+关注,谢谢! 有问题留言,我都会及时回复。
转载自:https://juejin.cn/post/7037507200163512351