Flutter 只需要几步就可以完成级联索引列表(仿微信通讯录/城市索引列表)

Flutter 只需要几步就可以完成级联索引列表(仿微信通讯录/城市索引列表)

前言
说实话,由于项目时间紧迫,秉承着C+V大法的理念,就想着在网上找个能用的简单点的索引列表代码改改直接来用,谁知道没一个靠谱的,靠人不如靠自己-.-

先上效果图

Flutter 只需要几步就可以完成级联索引列表(仿微信通讯录/城市索引列表)_第1张图片

注意

这套代码开发方式采用的是Getx的Bloc设计方式开发的,如果您使用的不是Getx,末尾也提供了常规代码方式。

实现步骤
  1. 创建数据Model
  2. 安装需要的插件
  3. 序列化排序后端给的“城市/通讯录/列表”数据
  4. 组装要展示的列表数据
  5. 画UI
一、创建Model:pinyin.dart
// 管理拼音
class CountryModel {
  // 详细数据
  var info;
  // 拼音首字母
  late var pin;
  CountryModel({
    this.info,
    this.pin,
  });
}

// 管理列表
class SortList {
  var label;
  var children;
  SortList({
    this.label,
    this.children,
  });
}

二、安装需要的插件

lpinyin帮助我们将后端数据转为拼音
scrollable_positioned_list帮助我们定位具体的item位置

lpinyin: ^2.0.3
scrollable_positioned_list: ^0.3.8
三、代码层面
state.dart
class IndexNavigationListState {
  late int currentIndex;
  IndexNavigationListState() {
    currentIndex = 0;
  }
}

logic.dart

数据采用的是本地的一个Map数据,具体大家换成自己后台数据就好了
Flutter 只需要几步就可以完成级联索引列表(仿微信通讯录/城市索引列表)_第2张图片

import 'package:get/get.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';

import 'state.dart';
import 'dart:convert';

import '../../config/pinyin.dart';
import '../../model/model.dart';
import 'package:lpinyin/lpinyin.dart';

class IndexNavigationListLogic extends GetxController {
  final IndexNavigationListState state = IndexNavigationListState();
  final ItemScrollController scrollController = ItemScrollController();
  List<CountryModel> CountryList = [];
  List<SortList> dataList = [
    SortList(label: '*', children: ['热门城市', '收藏城市'])
  ];
  List indexList = [];
	
// 右侧的索引列表如果您想循环全部的单词,就使用这个值代替UI层中的logic.dataList,目前是根据列表有什么数据,生成什么样的右侧索引
  final List<String> az = [
    "*",
    "A",
    "B",
    "C",
    "D",
    "E",
    "F",
    "G",
    "H",
    "I",
    "G",
    "K",
    "L",
    "M",
    "N",
    "O",
    "P",
    "Q",
    "R",
    "S",
    "T",
    "U",
    "V",
    "W",
    "X",
    "Y",
    "Z",
  ];

  
  void onInit() {
    // TODO: implement onInit
    super.onInit();

    getData();
  }

  getData() async {
    var _resultList = json.decode(schoolData);

    _resultList['schools'].forEach((element) {
      String now = '#';
      String nowPin = PinyinHelper.getFirstWordPinyin(element['province_name']);
      if (nowPin.length > 0) {
        now = indexList.contains(nowPin.substring(0, 1).toUpperCase())
            ? '#'
            : nowPin.substring(0, 1);
      }
      CountryModel vo =
          CountryModel(info: element['province_name'], pin: now.toUpperCase());
      CountryList.add(vo);
    });

    // 排序
    CountryList.sort((CountryModel a, CountryModel b) {
      if (a.pin == '#') {
        return 1;
      }
      if (b.pin == '#') {
        return 0;
      }
      return a.pin.compareTo(b.pin);
    });

    // 去重排序
    List _arr = [];
    var s = Set();
    for (var i = 0; i < CountryList.length; i++) {
      _arr.add(CountryList[i].pin);
    }
    s.addAll(_arr);

    // 转为Model类型列表
    s.toList().forEach((element) {
      element = SortList(label: element, children: []);
      dataList.add(element);
    });

    // 生成UI列表显示数据
    for (var element in dataList) {
      for (var item in CountryList) {
        if (element.label == item.pin) {
          element.children.add(item.info);
        }
      }
    }
  }

  // 改变索引下标
  void changeIndex(index) {
    state.currentIndex = index;
    update();
  }
}

view.dart

代码中的颜色:AppColor.themeColor是我的全局的颜色,大家自己替换掉就好了

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';

import 'logic.dart';

class IndexNavigationListPage extends StatelessWidget {
  IndexNavigationListPage({Key? key}) : super(key: key);

  final logic = Get.put(IndexNavigationListLogic());
  final state = Get.find<IndexNavigationListLogic>().state;

  
  Widget build(BuildContext context) {
    return GetBuilder<IndexNavigationListLogic>(
      builder: (logic) {
        return Expanded(
            child: Stack(
          children: [
            Listener(
              onPointerUp: (e) {
                // 拉动列表,取消右侧索引
                logic.changeIndex(-1);
              },
              child: ScrollablePositionedList.builder(
                physics: const BouncingScrollPhysics(),
                itemScrollController: logic.scrollController,
                padding: const EdgeInsets.all(0),
                itemBuilder: (context, index) {
                  return ListItem(logic: logic, index: index);
                },
                itemCount: logic.dataList.length,
              ),
            ),
            Positioned(
              width: 40.w,
              height: 540.h,
              right: 0,
              child: Listener(
                // 手指按下触发
                onPointerDown: (PointerDownEvent e) {
                  //打印手指按下的位置(相对于屏幕)
                  int i = e.localPosition.dy ~/ 21;
                  logic.changeIndex(i);
                  logic.scrollController.jumpTo(index: i);
                },
                // 手指滑动时触发
                onPointerMove: (PointerMoveEvent e) {
                  //用户手指滑动时,更新偏移
                  int i = e.localPosition.dy ~/ 21;
                  if (i >= 0 && i <= logic.az.length - 1) {
                    if (i != logic.state.currentIndex) {
                      logic.changeIndex(i);
                    }
                  }
                },
                child: ListView.builder(
                  padding: const EdgeInsets.all(0),
                  itemCount: logic.dataList.length,
                  itemBuilder: (BuildContext context, int index) {
                    return SizedBox(
                      height: 20.h,
                      child: Container(
                        alignment: Alignment.center,
                        decoration: BoxDecoration(
                            // borderRadius: BorderRadius.circular(20),
                            color: logic.state.currentIndex == index
                                ? const Color(0xffF3F4F7)
                                : null,
                            shape: BoxShape.circle),
                        child: logic.dataList[index].label == '*'
                            ? Container(
                                width: 8.w,
                                height: 8.h,
                                decoration: BoxDecoration(
                                    color: logic.state.currentIndex == index
                                        ? const Color(0xffFFCF57)
                                        : const Color(0xff9299A0),
                                    borderRadius: BorderRadius.circular(20)),
                              )
                            : Text(
                                logic.dataList[index].label,
                                style: TextStyle(
                                  fontFamily: 'sansBold',
                                  color: logic.state.currentIndex == index
                                      ? AppColor.themeColor
                                      : const Color(0xff9299A0),
                                ),
                              ),
                      ),
                    );
                  },
                ),
              ),
            )
          ],
        ));
      },
    );
  }
}

class ListItem extends StatelessWidget {
  var logic;
  var index;
  ListItem({super.key, required this.logic, this.index});

  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          padding: const EdgeInsetsDirectional.only(start: 10),
          color: Colors.grey.shade300,
          height: 30,
          width: double.infinity,
          alignment: Alignment.centerLeft,
          child: Text(logic.dataList[index].label),
        ),
        Container(
          padding: const EdgeInsetsDirectional.only(start: 15),
          child: ListView.builder(
              // 禁用滑动事件
              physics: const NeverScrollableScrollPhysics(),
              shrinkWrap: true,
              padding: const EdgeInsets.all(0),
              itemBuilder: (context, cityIndex) {
                return InkWell(
                  child: Container(
                    height: 40,
                    child: Column(
                      children: [
                        Expanded(
                          child: Container(
                            alignment: Alignment.centerLeft,
                            child:
                                Text(logic.dataList[index].children[cityIndex]),
                          ),
                        ),
                        const Divider(
                          height: 0.5,
                        )
                      ],
                    ),
                  ),
                  onTap: () {},
                );
              },
              itemCount: logic.dataList[index].children.length),
        )
      ],
    );
  }
}

OK,大功告成,完美实现索引列表功能!有问题,给我留言~

常规代码写法
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';

import '../config/const.dart';
import '../config/pinyin.dart';
import '../model/model.dart';

class IndexNavigationList extends StatefulWidget {
  final bool isShowRegional;
  const IndexNavigationList({super.key, this.isShowRegional = true});

  
  State<IndexNavigationList> createState() => _IndexNavigationListState();
}

class _IndexNavigationListState extends State<IndexNavigationList> {
  int currentIndex = 0;

  final ItemScrollController scrollController = ItemScrollController();
  List<CountryModel> CountryList = [];
  late List<SortList> dataList = [];
  List indexList = [];

  final List<String> az = [
    "*",
    "A",
    "B",
    "C",
    "D",
    "E",
    "F",
    "G",
    "H",
    "I",
    "G",
    "K",
    "L",
    "M",
    "N",
    "O",
    "P",
    "Q",
    "R",
    "S",
    "T",
    "U",
    "V",
    "W",
    "X",
    "Y",
    "Z",
  ];

  
  void initState() {
    // TODO: implement initState
    super.initState();
    if (widget.isShowRegional) {
      dataList.add(SortList(label: '*', children: [
        'Europe (35)',
        'APAC (15)',
        'Global (115)',
        'Nordic (4)',
        'SEA-Oceania(8)'
      ]));
    }
    getData();
  }

  getData() async {
    var _resultList = json.decode(schoolData);

    _resultList['schools'].forEach((element) {
      String now = '#';
      String nowPin = PinyinHelper.getFirstWordPinyin(element['province_name']);
      if (nowPin.length > 0) {
        now = indexList.contains(nowPin.substring(0, 1).toUpperCase())
            ? '#'
            : nowPin.substring(0, 1);
      }
      CountryModel vo =
          CountryModel(info: element['province_name'], pin: now.toUpperCase());
      CountryList.add(vo);
    });

    // 排序
    CountryList.sort((CountryModel a, CountryModel b) {
      if (a.pin == '#') {
        return 1;
      }
      if (b.pin == '#') {
        return 0;
      }
      return a.pin.compareTo(b.pin);
    });

    // 去重排序
    List _arr = [];
    var s = Set();
    for (var i = 0; i < CountryList.length; i++) {
      _arr.add(CountryList[i].pin);
    }
    s.addAll(_arr);

    // 转为Model类型列表
    s.toList().forEach((element) {
      element = SortList(label: element, children: []);
      dataList.add(element);
    });

    // 生成UI列表显示数据
    for (var element in dataList) {
      for (var item in CountryList) {
        if (element.label == item.pin) {
          element.children.add(item.info);
        }
      }
    }
  }

  // 改变索引下标
  void changeIndex(index) {
    currentIndex = index;
    setState(() {});
  }

  
  Widget build(BuildContext context) {
    return Expanded(
        child: Stack(
      children: [
        GestureDetector(
          onVerticalDragCancel: () => changeIndex(-1),
          child: ScrollablePositionedList.builder(
            physics: const BouncingScrollPhysics(),
            itemScrollController: scrollController,
            padding:
                EdgeInsets.only(top: 0, bottom: 30.h, left: 30.w, right: 30.w),
            itemBuilder: (context, index) {
              return ListItem(logic: {
                'dataList': dataList,
              }, index: index, isShowRegional: widget.isShowRegional);
            },
            itemCount: dataList.length,
          ),
        ),
        Positioned(
          width: 40.w,
          height: 540.h,
          right: 0,
          child: Listener(
            // 手指按下触发
            onPointerDown: (PointerDownEvent e) {
              //打印手指按下的位置(相对于屏幕)
              int i = e.localPosition.dy ~/ 21;
              changeIndex(i);
              scrollController.jumpTo(index: i);
            },
            // 手指滑动时触发
            onPointerMove: (PointerMoveEvent e) {
              //用户手指滑动时,更新偏移
              int i = e.localPosition.dy ~/ 21;
              if (i >= 0 && i <= dataList.length - 1) {
                if (i != currentIndex) {
                  changeIndex(i);
                }
              }
            },
            child: ListView.builder(
              padding: const EdgeInsets.all(0),
              itemCount: dataList.length,
              itemBuilder: (BuildContext context, int index) {
                return SizedBox(
                  height: 20.h,
                  child: Container(
                    alignment: Alignment.center,
                    decoration: BoxDecoration(
                        // borderRadius: BorderRadius.circular(20),
                        color: currentIndex == index
                            ? const Color(0xffF3F4F7)
                            : null,
                        shape: BoxShape.circle),
                    child: dataList[index].label == '*'
                        ? Container(
                            width: 8.w,
                            height: 8.h,
                            decoration: BoxDecoration(
                                color: currentIndex == index
                                    ? const Color(0xffFFCF57)
                                    : const Color(0xff9299A0),
                                borderRadius: BorderRadius.circular(20)),
                          )
                        : Text(
                            dataList[index].label,
                            style: TextStyle(
                              fontFamily: 'sansBold',
                              color: currentIndex == index
                                  ? AppColor.themeColor
                                  : const Color(0xff9299A0),
                            ),
                          ),
                  ),
                );
              },
            ),
          ),
        )
      ],
    ));
  }
}

class ListItem extends StatelessWidget {
  var logic;
  var index;
  bool isShowRegional;
  ListItem(
      {super.key, required this.logic, this.index, this.isShowRegional = true});

  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
            margin: EdgeInsets.only(
                top: index != 0 ? 38.h : 0, left: 12.w, bottom: 10.h),
            width: double.infinity,
            alignment: Alignment.centerLeft,
            child: Text(
              logic['dataList'][index].label == '*'
                  ? 'Regional Plans'
                  : logic['dataList'][index].label,
              style: TextStyle(
                  fontFamily: 'sansBold',
                  color: const Color(0xff768089),
                  fontSize: 14.sp),
            )),
        Container(
          child: ListView.builder(
              // 禁用滑动事件
              physics: const NeverScrollableScrollPhysics(),
              shrinkWrap: true,
              padding: const EdgeInsets.all(0),
              itemBuilder: (context, cityIndex) {
                return InkWell(
                  child: Container(
                    height: 40.h,
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: [
                        Container(
                          width: 33.w,
                          height: 33.h,
                          margin: EdgeInsets.only(right: 20.w),
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.circular(50),
                            color: const Color(0xffE9EBF0),
                          ),
                        ),
                        Expanded(
                          child: Container(
                            alignment: Alignment.centerLeft,
                            child: Text(
                              logic['dataList'][index].children[cityIndex],
                              style: TextStyle(
                                  fontSize: 15.sp,
                                  color: const Color(0xff15141F),
                                  fontFamily: 'sansBold'),
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                  onTap: () {},
                );
              },
              itemCount: logic['dataList'][index].children.length),
        )
      ],
    );
  }
}

你可能感兴趣的:(flutter,微信,android,城市索引列表,微信通讯录)