城市地区选择器 级联选择器

发布时间 2023-09-09 16:37:18作者: papering

 

级联选择 Cascader - Ant Design https://ant-design.gitee.io/components/cascader-cn

city_picker_china | Flutter Package https://pub-web.flutter-io.cn/packages/city_picker_china

flutter_city_picker/lib/src/picker.dart at master · cenumi/flutter_city_picker · GitHub https://github.com/cenumi/flutter_city_picker/blob/master/lib/src/picker.dart

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:collection/collection.dart';

const _nameChildren = 'children';

const _nameCode = 'code';

const _nameName = 'name';

/// The result object returned by Navigator
class CityResult {
  const CityResult({
    required this.code,
    required this.province,
    this.city,
    this.county,
  });

  final String code;
  final String province;
  final String? city;
  final String? county;

  @override
  String toString() {
    return '$province${city == null ? '' : ',$city'}${county == null ? '' : ',$county'}';
  }
}

/// Data object for serialization
class CityNode {
  CityNode(this.name, this.code, [this.children]);

  final String name;
  final String code;
  final List<CityNode>? children;

  factory CityNode.fromJson(Map<String, dynamic> json) {
    final leaf = json[_nameChildren] == null;

    return CityNode(
      json[_nameName] as String,
      json[_nameCode] as String,
      leaf ? null : fromJsonList(json[_nameChildren] as List<dynamic>),
    );
  }

  static List<CityNode> fromJsonList(List<dynamic> list) {
    return list
        .map((e) => CityNode.fromJson(e as Map<String, dynamic>))
        .toList();
  }
}

/// The picker widget
///
/// Normally put it in a dialog or bottomsheet
class CityPicker extends StatefulWidget {
  /// Default constructor
  ///
  /// All column index will be 0
  const CityPicker({Key? key})
      : code = null,
        province = null,
        city = null,
        county = null,
        super(key: key);

  /// Construct the picker by name provided
  ///
  /// The picker column index will stop at the name provided
  /// If any property is null or not found in dataset, the index after will be 0
  const CityPicker.fromName({Key? key, this.province, this.city, this.county})
      : code = null,
        super(key: key);

  /// Construct the pick by code provided
  ///
  /// The picker column index will stop at the code provided
  /// if code is not in dataset, all column index will be 0
  const CityPicker.fromCode({Key? key, this.code})
      : province = null,
        city = null,
        county = null,
        super(key: key);

  /// The city code, e.g. 110111
  final String? code;

  /// The province name, e.g. 北京市
  final String? province;

  /// The city name, e.g. 北京市
  final String? city;

  /// The county name, e.g. 房山区
  final String? county;

  /// Data set from baidu 202104
  /// rootBundle.loadStructuredData will cache the result so no worries
  static Future<List<dynamic>> loadAssets() {
    return rootBundle.loadStructuredData(
      'packages/city_picker_china/assets/data_202104.json',
      (str) async => jsonDecode(str),
    );
  }

  /// Convert the data set to structured data
  static Future<List<CityNode>> loadCityNodes() async {
    return CityNode.fromJsonList(await loadAssets());
  }

  /// Search city infomation by code
  ///
  /// If code is null or not in dataset, null is returned
  static Future<CityResult?> searchWithCode(
    String? code, {
    List<dynamic>? dataSet,
  }) async {
    if (code == null || code.isEmpty) {
      return null;
    }

    final provinces = dataSet ?? await loadAssets();

    for (final province in provinces) {
      if (province[_nameCode] == code) {
        return CityResult(code: code, province: province[_nameName]);
      }
      for (final city in province[_nameChildren]) {
        if (city[_nameCode] == code) {
          return CityResult(
            code: code,
            province: province[_nameName],
            city: city[_nameName],
          );
        }

        for (final county in city[_nameChildren]) {
          if (county[_nameCode] == code) {
            return CityResult(
              code: code,
              province: province[_nameName],
              city: city[_nameName],
              county: county[_nameName],
            );
          }
        }
      }
    }
    return null;
  }

  /// Search city infomation by names
  ///
  /// If [province] is null or not in dataset, null is returned
  /// If any other name is null or not in dataset, the corresponding will be null
  static Future<CityResult?> searchWithName({
    String? province,
    String? city,
    String? county,
    List<dynamic>? dataSet,
  }) async {
    if (province == null || province.isEmpty) {
      return null;
    }

    final provinces = dataSet ?? await loadAssets();

    final provinceInfo = provinces.firstWhereOrNull(
      (element) => element[_nameName] == province,
    );

    if (provinceInfo == null) {
      return null;
    }

    final cityInfo = (city == null || city.isEmpty)
        ? null
        : (provinceInfo[_nameChildren] as List<dynamic>).firstWhereOrNull(
            (element) => element[_nameName] == city,
          );

    if (cityInfo == null) {
      return CityResult(code: provinceInfo[_nameCode], province: province);
    }

    final countyInfo = (county == null || county.isEmpty)
        ? null
        : (cityInfo[_nameChildren] as List<dynamic>).firstWhereOrNull(
            (element) => element[_nameName] == county,
          );

    if (countyInfo == null) {
      return CityResult(
        code: cityInfo[_nameCode],
        province: province,
        city: city,
      );
    }

    return CityResult(
      code: countyInfo[_nameCode],
      province: province,
      city: city,
      county: county,
    );
  }

  @override
  State<CityPicker> createState() => _CityPickerState();
}

class _CityPickerState extends State<CityPicker> {
  List<dynamic>? _data;

  List<String>? _provinces;
  List<String>? _cities;
  List<String>? _counties;

  final _provinceController = FixedExtentScrollController();
  final _cityController = FixedExtentScrollController();
  final _countyController = FixedExtentScrollController();

  @override
  void initState() {
    super.initState();
    () async {
      _data = await CityPicker.loadAssets();

      final result = widget.code != null
          ? await CityPicker.searchWithCode(widget.code!, dataSet: _data)
          : await CityPicker.searchWithName(
              province: widget.province,
              city: widget.city,
              county: widget.county,
              dataSet: _data,
            );

      int indexProvince = 0;
      int indexCity = 0;
      int indexCounty = 0;

      _provinces = _data!.mapIndexed<String>((index, element) {
        final name = element[_nameName];
        if (name == result?.province) {
          indexProvince = index;
        }
        return name;
      }).toList();

      final dataCities = _data![indexProvince][_nameChildren] as List<dynamic>;
      _cities = dataCities.mapIndexed<String>((index, element) {
        final name = element[_nameName];
        if (name == result?.city) {
          indexCity = index;
        }
        return name;
      }).toList();

      final dataCounties = _data![indexProvince][_nameChildren][indexCity]
          [_nameChildren] as List<dynamic>;
      _counties = dataCounties.mapIndexed<String>((index, element) {
        final name = element[_nameName];
        if (name == result?.county) {
          indexCounty = index;
        }
        return name;
      }).toList();

      setState(() {});

      SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
        _provinceController.jumpToItem(indexProvince);
        _cityController.jumpToItem(indexCity);
        _countyController.jumpToItem(indexCounty);
      });
    }();
  }

  @override
  void dispose() {
    _provinceController.dispose();
    _cityController.dispose();
    _countyController.dispose();
    super.dispose();
  }

  void _updateCities() {
    final list = _data![_provinceController.selectedItem][_nameChildren]
        as List<dynamic>;
    _cities = list.map<String>((e) => e[_nameName]).toList();
  }

  void _updateCounties() {
    final list = _data![_provinceController.selectedItem][_nameChildren]
        [_cityController.selectedItem][_nameChildren] as List<dynamic>;
    _counties = list.map<String>((e) => e[_nameName]).toList();
  }

  @override
  Widget build(BuildContext context) {
    final localization = MaterialLocalizations.of(context);

    return Column(
      children: [
        ButtonBar(
          alignment: MainAxisAlignment.spaceBetween,
          children: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: Text(localization.cancelButtonLabel),
            ),
            TextButton(
              onPressed: () {
                CityResult? result;
                if (_data == null) {
                  result = null;
                } else {
                  final indexProvince = _provinceController.selectedItem;
                  final indexCity = _cityController.selectedItem;
                  final indexCounty = _countyController.selectedItem;

                  result = CityResult(
                    code: _data![indexProvince][_nameChildren][indexCity]
                        [_nameChildren][indexCounty][_nameCode],
                    province: _provinces![indexProvince],
                    city: _cities![indexCity],
                    county: _counties![indexCounty],
                  );
                }
                Navigator.pop(context, result);
              },
              child: Text(localization.okButtonLabel),
            ),
          ],
        ),
        Expanded(
          child: Row(
            children: [
              _Picker(
                controller: _provinceController,
                data: _provinces ?? [],
                onSelectedItemChanged: () {
                  _cityController.jumpTo(0);
                  _countyController.jumpTo(0);
                  setState(() {
                    _updateCities();
                    _updateCounties();
                  });
                },
              ),
              _Picker(
                controller: _cityController,
                data: _cities ?? [],
                onSelectedItemChanged: () {
                  _countyController.jumpTo(0);
                  setState(() {
                    _updateCounties();
                  });
                },
              ),
              _Picker(
                controller: _countyController,
                data: _counties ?? [],
                onSelectedItemChanged: () {},
              ),
            ],
          ),
        ),
      ],
    );
  }
}

class _Picker extends StatelessWidget {
  const _Picker({
    Key? key,
    required this.data,
    required this.onSelectedItemChanged,
    required this.controller,
  }) : super(key: key);

  final List<String> data;
  final VoidCallback onSelectedItemChanged;
  final FixedExtentScrollController controller;

  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: CupertinoPicker.builder(
        itemExtent: 40,
        scrollController: controller,
        onSelectedItemChanged: (_) => onSelectedItemChanged(),
        itemBuilder: (_, index) => Center(
          child: FittedBox(
            child: Text(data[index], style: const TextStyle(fontSize: 14)),
          ),
        ),
        childCount: data.length,
      ),
    );
  }
}