flutter實現ListView頭部懸浮,item多種高度

須要封裝好的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 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

相關文章
相關標籤/搜索