flutter:實(shí)現(xiàn)多區(qū)頭多行懸停效果(2)

Untitled1.gif

最近app想要類似于ios的那種tableview,區(qū)頭懸停的效果。最后找到了一個(gè)大神的方案。比較不錯(cuò),UI效果也達(dá)到了理想效果。
具體的實(shí)現(xiàn)代碼:
主要是gsy大神的這個(gè)類:(地址:https://github.com/CarGuo/gsy_flutter_demo

引入的頭文件
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
準(zhǔn)備好數(shù)據(jù)源:
Map<String, List<String>> moreItemSectionList = {
  '拜訪一區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "店前拜訪提醒"],
  '拜訪二區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "店前拜訪提醒", "1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行"],
  '拜訪三區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "店前拜訪提醒", "1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行", "3-店鋪檢查"],
  '拜訪四區(qū)': ["遠(yuǎn)程拜訪"],
  '拜訪五區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "店前拜訪提醒", "1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行", "3-店鋪檢查"],
  '拜訪六區(qū)': ["1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行", "3-店鋪檢查"],
  '拜訪⑦區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "店前拜訪提醒", "1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行", "3-店鋪檢查"],
  '拜訪⑧區(qū)': ["3-店鋪檢查"],
  '拜訪⑨區(qū)': ["遠(yuǎn)程拜訪", "遠(yuǎn)程訂單", "1-進(jìn)入門店", "2-生動(dòng)化執(zhí)行", "3-店鋪檢查"],
};

實(shí)現(xiàn)方法:
final random = math.Random();
const stickHeader = 50.0;

class StickSliverListDemoPage extends StatefulWidget {
  //整理數(shù)據(jù)
  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('數(shù)據(jù)----->$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('數(shù)據(jù)----->$_titleIndex');
    return Scaffold(
         appBar: AppBar(
         title: new Text("分區(qū)列表"),
        ),
        body: Stack(
      children: <Widget>[
        Container(
          child: CustomScrollView(
            key: scrollKey,
            controller: _scrollController,
            physics: const ClampingScrollPhysics(),
            slivers: List.generate(widget.dataList.length, (index) {
              // Log.i('數(shù)據(jù)----->$index');
              //分區(qū)的數(shù)據(jù)
              ExpendedModel sectionModel =
                  widget.dataList[index] as ExpendedModel;
              Log.i('數(shù)據(jù)----->$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) {
    //展開和控制的邏輯,不需要這個(gè)效果
    bool needExpanded = (widget.expendedModel!.dataList.length > 3);
    needExpanded = false;
    List cellList = widget.expendedModel!.dataList;
    Log.i('cell數(shù)據(jù)------>$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數(shù)據(jù)------>$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.第一步改造數(shù)據(jù)源

構(gòu)造數(shù)據(jù)這一步很重要,因?yàn)榭蚣芾锩娴臄?shù)據(jù)是比較特殊,那我們的后臺(tái)下發(fā)的數(shù)據(jù)模型肯定和框架不一樣,那我們?nèi)绾胃脑鞌?shù)據(jù)呢?

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);
  });

其實(shí)就是這段方法,將我們的數(shù)據(jù)源改成ExpendedModel類型的數(shù)據(jù),而你分析了ExpendedModel就會(huì)發(fā)現(xiàn),dataList是分區(qū)的list數(shù)據(jù),sectionTitle是區(qū)頭,也就是key值列表,key值列表的數(shù)據(jù)是來源于moreItemSectionList.keys.toList()這個(gè)方法。

2.第二步就是構(gòu)造組件

仔細(xì)分析代碼發(fā)現(xiàn),這個(gè)列表構(gòu)造不是用listview.build()方法來構(gòu)造的,而是用Stack組件,CustomScrollView子組件和自定義的StickHeader組件構(gòu)建這樣一個(gè)區(qū)頭列表的。

slivers: List.generate(widget.dataList.length, (index) {
              // Log.i('數(shù)據(jù)----->$index');
              //分區(qū)的數(shù)據(jù)
              ExpendedModel sectionModel =
                  widget.dataList[index] as ExpendedModel;
              Log.i('數(shù)據(jù)----->$sectionModel.dataList');
              return SliverExpandedList(
                sectionModel,
                "header $index",
                visibleCount: sectionModel.dataList.length,
                valueChanged: (_) {
                  setState(() {});
                },
              );
            }),

這是構(gòu)造每個(gè)區(qū)的列表方法。就是用 List.generate方法構(gòu)造每一行子組件。

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數(shù)據(jù)------>$cellValue');

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

childCount是行數(shù)計(jì)算。
很多人疑問為什么要判斷index = 0,因?yàn)槲野l(fā)現(xiàn)這個(gè)框架會(huì)把區(qū)頭的key,也算在行數(shù)計(jì)算上,第一行其實(shí)是區(qū)頭,后面的才是行數(shù)據(jù)。取值的時(shí)候要取index- 1的索引值,否則就不對(duì)了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容