likes
comments
collection
share

Flutter 自定义数据选择器

作者站长头像
站长
· 阅读数 17

前言

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 行业数据

Flutter 自定义数据选择器

area-data.dart 省市区数据

Flutter 自定义数据选择器

areaData 需要重新组装数据 id-> value name->label

效果:

1.选中数据再次点击选择器数据回显。 2.可以动态显示多列 3.多列显示实现数据的联动

Flutter 自定义数据选择器

项目源码 github.com/chenbing11/… master分支 有需要请自取

结语

创作不易,请多点赞+关注,谢谢! 有问题留言,我都会及时回复。