flutter:实现多区头多行悬停效果(2)

Untitled1.gif

最近app想要类似于ios的那种tableview,区头悬停的效果。最后找到了一个大神的方案。比较不错,UI效果也达到了理想效果。
具体的实现代码:
主要是gsy大神的这个类:(地址:https://github.com/CarGuo/gsy_flutter_demo

引入的头文件
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
准备好数据源:
Map<String, List<String>> moreItemSectionList = {
  '拜访一区': ["远程拜访", "远程订单", "店前拜访提醒"],
  '拜访二区': ["远程拜访", "远程订单", "店前拜访提醒", "1-进入门店", "2-生动化执行"],
  '拜访三区': ["远程拜访", "远程订单", "店前拜访提醒", "1-进入门店", "2-生动化执行", "3-店铺检查"],
  '拜访四区': ["远程拜访"],
  '拜访五区': ["远程拜访", "远程订单", "店前拜访提醒", "1-进入门店", "2-生动化执行", "3-店铺检查"],
  '拜访六区': ["1-进入门店", "2-生动化执行", "3-店铺检查"],
  '拜访⑦区': ["远程拜访", "远程订单", "店前拜访提醒", "1-进入门店", "2-生动化执行", "3-店铺检查"],
  '拜访⑧区': ["3-店铺检查"],
  '拜访⑨区': ["远程拜访", "远程订单", "1-进入门店", "2-生动化执行", "3-店铺检查"],
};

实现方法:
final random = math.Random();
const stickHeader = 50.0;

class StickSliverListDemoPage extends StatefulWidget {
  //整理数据
  final List<ExpendedModel?> dataList =
      List.generate(moreItemSectionList.length, (index) {
    final List _titles = moreItemSectionList.keys.toList();
    String titlekey = _titles[index];
    List cellList = moreItemSectionList[titlekey] as List;
    return ExpendedModel(false, cellList, titlekey);
  });

  @override
  _StickSliverListDemoPageState createState() =>
      _StickSliverListDemoPageState();
}

class _StickSliverListDemoPageState extends State<StickSliverListDemoPage> {
  int _titleIndex = 0;
  bool _showTitleTopButton = false;

  ScrollController _scrollController = new ScrollController();

  final GlobalKey scrollKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    Log.i('数据----->$widget.dataList');

    _scrollController.addListener(scrollChanged);
  }

  @override
  void dispose() {
    super.dispose();
    _scrollController.removeListener(scrollChanged);
  }

  scrollChanged() {
    if (widget.dataList.length == 0) {
      return;
    }
    var item = widget.dataList.lastWhere((item) {
      if (item!.globalKey.currentContext == null) {
        return false;
      }

      ///获取 renderBox
      RenderSliver? renderSliver =
          item.globalKey.currentContext!.findRenderObject() as RenderSliver?;
      if (renderSliver == null) {
        return false;
      }
      return renderSliver.constraints.scrollOffset > 0;
    }, orElse: () {
      return null;
    });
    if (item == null) {
      return;
    }
    Log.i('----->$item');
    int currentIndex = widget.dataList.indexOf(item);
    if (currentIndex != _titleIndex) {
      setState(() {
        _titleIndex = currentIndex;
      });
    }
    var needTopButton = _scrollController.position.pixels > 0;
    if (needTopButton != _showTitleTopButton) {
      setState(() {
        _showTitleTopButton = needTopButton;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    Log.i('数据----->$_titleIndex');
    return Scaffold(
         appBar: AppBar(
         title: new Text("分区列表"),
        ),
        body: Stack(
      children: <Widget>[
        Container(
          child: CustomScrollView(
            key: scrollKey,
            controller: _scrollController,
            physics: const ClampingScrollPhysics(),
            slivers: List.generate(widget.dataList.length, (index) {
              // Log.i('数据----->$index');
              //分区的数据
              ExpendedModel sectionModel =
                  widget.dataList[index] as ExpendedModel;
              Log.i('数据----->$sectionModel.dataList');
              return SliverExpandedList(
                sectionModel,
                "header $index",
                visibleCount: sectionModel.dataList.length,
                valueChanged: (_) {
                  setState(() {});
                },
              );
            }),
          ),
        ),
        StickHeader(
          "header $_titleIndex",
          showTopButton: _showTitleTopButton,
          callback: () {
            var item = widget.dataList[_titleIndex]!;
            RenderSliver renderSliver = item.globalKey.currentContext!
                .findRenderObject() as RenderSliver;
            var position = _scrollController.position.pixels -
                renderSliver.constraints.scrollOffset;
            _scrollController.position.jumpTo(position);
          },
          sectionModel: widget.dataList[_titleIndex] as ExpendedModel,
        )
      ],
    ));
  }
}

class SliverExpandedList extends StatefulWidget {
  final ExpendedModel? expendedModel;
  final String title;
  final int visibleCount;
  final ValueChanged? valueChanged;

  SliverExpandedList(this.expendedModel, this.title,
      {required this.visibleCount, this.valueChanged});

  @override
  _SliverExpandedListState createState() => _SliverExpandedListState();
}

class _SliverExpandedListState extends State<SliverExpandedList> {
  bool expanded = false;

  toTop() {
    
  }

  getListCount(bool needExpanded) {
    return (expanded)
        ? (needExpanded)
            ? widget.expendedModel!.dataList.length + 2
            : widget.expendedModel!.dataList.length + 1
        : (needExpanded)
            ? widget.visibleCount + 2
            : widget.visibleCount + 1;
  }

  @override
  Widget build(BuildContext context) {
    //展开和控制的逻辑,不需要这个效果
    bool needExpanded = (widget.expendedModel!.dataList.length > 3);
    needExpanded = false;
    List cellList = widget.expendedModel!.dataList;
    Log.i('cell数据------>$cellList');

    return SliverList(
      key: widget.expendedModel!.globalKey,
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          // ///增加bottom
          // if (!expanded && needExpanded && index == widget.visibleCount + 1) {
          //   return renderExpendedMore();
          // }
          // if (index == widget.expendedModel!.dataList.length + 1) {
          //   return renderExpendedMore();
          // }
          // Log.i('cell索引------>$index');

          ///增加header
          if (index == 0) {
            return StickHeader(widget.title,
                sectionModel: widget.expendedModel as ExpendedModel);
          }
          String cellValue = cellList[index - 1] as String;
          Log.i('cell数据------>$cellValue');

          ///cell
          return Card(
            child: Container(
              height: 44.0,
              alignment: Alignment.centerLeft,
              child: Text(cellValue),
            ),
          );
        },
        childCount: getListCount(needExpanded),
      ),
    );
  }
}

class StickHeader extends StatelessWidget {
  final String title;
  final bool showTopButton;
  final VoidCallback? callback;
  final ExpendedModel sectionModel;
  StickHeader(this.title,
      {Key? key,
      this.showTopButton = false,
      required this.sectionModel,
      this.callback})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: stickHeader,
      color: Colors.deepPurple,
      padding: const EdgeInsets.only(left: 10.0),
      alignment: Alignment.centerLeft,
      child: Row(
        children: <Widget>[
          Expanded(
            child: Text(
              sectionModel.sectionTitle,
              style: const TextStyle(color: Colors.white),
            ),
          ),
          Visibility(
            visible: showTopButton,
            child: InkWell(
              onTap: () {
                callback?.call();
              },
              child: const Icon(
                Icons.vertical_align_top,
                color: Colors.red,
              ),
            ),
          )
        ],
      ),
    );
  }
}

class ExpendedModel {
  bool expended;

  List dataList;

  GlobalKey globalKey = GlobalKey();

  String sectionTitle;

  ExpendedModel(this.expended, this.dataList, this.sectionTitle);
}

1.第一步改造数据源

构造数据这一步很重要,因为框架里面的数据是比较特殊,那我们的后台下发的数据模型肯定和框架不一样,那我们如何改造数据呢?

final List<ExpendedModel?> dataList =
      List.generate(moreItemSectionList.length, (index) {
    final List _titles = moreItemSectionList.keys.toList();
    String titlekey = _titles[index];
    List cellList = moreItemSectionList[titlekey] as List;
    return ExpendedModel(false, cellList, titlekey);
  });

其实就是这段方法,将我们的数据源改成ExpendedModel类型的数据,而你分析了ExpendedModel就会发现,dataList是分区的list数据,sectionTitle是区头,也就是key值列表,key值列表的数据是来源于moreItemSectionList.keys.toList()这个方法。

2.第二步就是构造组件

仔细分析代码发现,这个列表构造不是用listview.build()方法来构造的,而是用Stack组件,CustomScrollView子组件和自定义的StickHeader组件构建这样一个区头列表的。

slivers: List.generate(widget.dataList.length, (index) {
              // Log.i('数据----->$index');
              //分区的数据
              ExpendedModel sectionModel =
                  widget.dataList[index] as ExpendedModel;
              Log.i('数据----->$sectionModel.dataList');
              return SliverExpandedList(
                sectionModel,
                "header $index",
                visibleCount: sectionModel.dataList.length,
                valueChanged: (_) {
                  setState(() {});
                },
              );
            }),

这是构造每个区的列表方法。就是用 List.generate方法构造每一行子组件。

3.第三步就是子组件SliverExpandedList的方法
 SliverList(
      key: widget.expendedModel!.globalKey,
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          ///增加header
          if (index == 0) {
            return StickHeader(widget.title,
                sectionModel: widget.expendedModel as ExpendedModel);
          }
          String cellValue = cellList[index - 1] as String;
          Log.i('cell数据------>$cellValue');

          ///cell
          return Card(
            child: Container(
              height: 44.0,
              alignment: Alignment.centerLeft,
              child: Text(cellValue),
            ),
          );
        },
        childCount: getListCount(needExpanded),
      ),

childCount是行数计算。
很多人疑问为什么要判断index = 0,因为我发现这个框架会把区头的key,也算在行数计算上,第一行其实是区头,后面的才是行数据。取值的时候要取index- 1的索引值,否则就不对了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容