須要封裝好的Widget能夠看這篇:app
flutter實現ListView分組頭部懸浮,支持混搭多種header、item、separator,支持indexPath跳轉
less
思路:ide
借住Stack,底層是ScrollView,ScrollView上有多個分組header(如下簡稱header)。上層是固定位置(懸停)的HoveringHeader。
在ScrollView向上滾動時,當某個header上邊緣和HoveringHeader下邊緣即將重疊時,HoveringHeader的位置開始隨着ScrollView的滾動而滾動。當該header上邊緣處於ScrollView可顯示區域的頂部時,還原HoveringHeader的位置,並把HoveringHeader的內容更新爲該header的內容,以達到視覺欺騙的效果。
ui
upward.pngthis
在ScrollView向下滾動時,當某個header上邊緣即將離開ScrollView可顯示區域的頂部時,更新HoveringHeader的位置,使其底邊位置處於ScrollView顯示區域的頂部。而且將其內容更新爲上一個header的內容。在HoveringHeader的頂邊位置和ScrollView顯示區域的頂部重疊以前,HoveringHeader隨着ScrollView滾動而滾動。當HoveringHeader的頂邊位置和ScrollView顯示區域的頂部重疊時,HoveringHeader迴歸到原始位置。pwa
downward.pngcode
效果:element
QQ20200918-095032.gifget
複製代碼能夠直接運行查看效果,爲了更直觀,用不一樣顏色區分上層懸停的header和ScrollView上的header。it
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'dart:ui'; class XBTestPage extends StatefulWidget { @override _XBTestPageState createState() => _XBTestPageState(); } class _XBTestPageState extends State<XBTestPage> { HoverHeaderVM _hoverVM = HoverHeaderVM(); ScrollController _controller = ScrollController(); List<HoverOffsetInfo> _hoverOffsetInfoList = []; int _hoverOffsetInfoIndex = 0; List _titles = _dataSource.keys.toList(); double _lastOffset = 0; static Map<String, List<String>> _dataSource = { "2020": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "2021": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "2022": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "2023": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "2024": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "2025": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "2026": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "2027": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "2028": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "2029": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], }; @override void initState() { super.initState(); _computeHoverOffsetInfo(); _controller.addListener(() { double offset = _controller.offset; bool show = offset >= 0; if (_hoverVM.show != show) { _hoverVM.show = show; } bool upward = offset - _lastOffset > 0; _lastOffset = offset; HoverOffsetInfo offsetInfo; if (_hoverOffsetInfoList.length > _hoverOffsetInfoIndex) { offsetInfo = _hoverOffsetInfoList[_hoverOffsetInfoIndex]; // print( // "start offset:${offsetInfo.startOffset},end offset:${offsetInfo.endOffset},_hoverOffsetInfoIndex:$_hoverOffsetInfoIndex"); if (upward) { ///向上滾動 if (offset < offsetInfo.startOffset) { /// [sectionStartOffset,startOffset) if (_hoverVM.offset != 0) { print("1"); _hoverVM.update(offsetInfo.prevTitle, 0); } } else if (offset > offsetInfo.endOffset) { ///(endOffset ///超過endOffset,切換到下一個offsetInfo print("2"); _hoverOffsetInfoIndex++; if (_hoverOffsetInfoIndex >= _hoverOffsetInfoList.length) { _hoverOffsetInfoIndex = _hoverOffsetInfoList.length - 1; } _hoverVM.update(offsetInfo.title, 0); } else { /// [startOffset,endOffset] print("3"); _hoverVM.update( offsetInfo.prevTitle, offset - offsetInfo.startOffset); } } else { ///向下滾動 if (offset >= offsetInfo.startOffset && offset <= offsetInfo.endOffset) { ///[startOffset,endOffset] _hoverVM.update( offsetInfo.prevTitle, offset - offsetInfo.startOffset); print( "4 _hoverVM.offset:${_hoverVM.offset},offset:$offset,offsetInfo.startOffset:${offsetInfo.startOffset}"); } else if (offset >= offsetInfo.sectionStartOffset) { ///[sectionStartOffset,startOffset) if (_hoverVM.offset != 0) { print("5"); _hoverVM.update(offsetInfo.prevTitle, 0); } } else { /// sectionStartOffset) /// 切換到上一個offsetInfo ///其實就是offset小於上一個offsetInfo的endOffset的狀況 print("6"); _hoverOffsetInfoIndex--; if (_hoverOffsetInfoIndex < 0) { _hoverOffsetInfoIndex = 0; } _hoverVM.update(offsetInfo.prevTitle, 0); } } } }); } @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (ctx) { return _hoverVM; }, child: Scaffold( appBar: AppBar( title: Text("header 懸浮"), ), body: Stack( children: <Widget>[ CustomScrollView( controller: _controller, slivers: List.generate(_titles.length, (titleIndex) { String title = _titles[titleIndex]; List<String> dataList = _dataSource[title]; return SliverList( delegate: SliverChildBuilderDelegate((ctx, cellIndex) { if (cellIndex == 0) { return Header(title); } else { int fixIndex = cellIndex - 1; String data = dataList[fixIndex]; return Cell(data); } }, childCount: dataList.length + 1)); }), ), Consumer(builder: (ctx, HoverHeaderVM hoverVM, child) { return Visibility( visible: hoverVM.show, child: Header(hoverVM.title, color: Colors.green, offset: -hoverVM.offset), ); }) ], ), ), ); } //計算須要懸停的區間信息 _computeHoverOffsetInfo() { _hoverOffsetInfoList.clear(); double totalOffset = 0; for (var i = 0; i < _titles.length; i++) { ///最後一段不須要計算 if (i != _titles.length - 1) { String element = _titles[i]; List<String> dataList = _dataSource[element]; double sectionStartOffset = totalOffset; double sectionOffset = dataList.length * Cell.height; double startOffset = sectionOffset + totalOffset; double endOffset = startOffset + Header.height; totalOffset += sectionOffset + Header.height; _hoverOffsetInfoList.add(HoverOffsetInfo( prevTitle: element, title: _titles[i + 1], startOffset: startOffset, endOffset: endOffset, sectionStartOffset: sectionStartOffset)); } } print(_hoverOffsetInfoList); } @override void dispose() { _controller.dispose(); super.dispose(); } } class Header extends StatelessWidget { static final height = 50.0; final String title; final Color color; final double offset; Header(this.title, {this.color, this.offset = 0}); @override Widget build(BuildContext context) { return Container( alignment: Alignment(-0.8, 0.0), height: height, child: Stack( children: <Widget>[ Positioned( top: offset, child: Container( height: height, width: MediaQueryData.fromWindow(window).size.width, color: color ?? Colors.orange, alignment: Alignment.centerLeft, child: Text( title, style: TextStyle(fontSize: 20), ), ), ), ], ), ); } } class Cell extends StatelessWidget { static final height = 80.0; final String title; Cell(this.title); @override Widget build(BuildContext context) { return Container( height: height, alignment: Alignment.center, color: Colors.black26, child: Column( children: <Widget>[ Expanded( child: Container( alignment: Alignment.center, child: Text(title), )), Container( height: 1, color: Colors.white, ) ], ), ); } } class HoverOffsetInfo { String prevTitle; String title; double startOffset; double endOffset; double sectionStartOffset; HoverOffsetInfo( {@required this.prevTitle, @required this.title, @required this.startOffset, @required this.endOffset, @required this.sectionStartOffset}); @override String toString() { return 'HoverOffsetInfo{prevTitle: $prevTitle, title: $title, startOffset: $startOffset, endOffset: $endOffset}'; } } class HoverHeaderVM extends ChangeNotifier { String _title = "2020"; bool _show = true; double _offset = 0; String get title => _title; double get offset => _offset; update(String title, double offset) { _title = title; _offset = offset; notifyListeners(); } bool get show => _show; set show(bool show) { _show = show; notifyListeners(); } }
做者:huisedediao 連接:https://www.jianshu.com/p/0e101d5614c2 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。