fluttercandies / flutter_scrollview_observer

A widget for observing data related to the child widgets being displayed in a ScrollView. Maintainer: @LinXunFeng
https://pub.dev/packages/scrollview_observer
MIT License
438 stars 47 forks source link

[How to use] 搭配SliverPersistentHeader的时候,需要怎么滚动定位呢? #95

Closed feimenggo closed 1 month ago

feimenggo commented 1 month ago

Platforms

Android, iOS, macOS, Windows

Description

点击分组可以展开或收起列表,现在想要定位到列表的指定项。

My code

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:scrollview_observer/scrollview_observer.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final scrollController = ScrollController();
  late final observerController =
      SliverObserverController(controller: scrollController);

  final List<ItemBean> items = ItemBean.groupListData;
  Set<BuildContext> sliverContextSets = {};

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 滚动到指定位置
          observerController.jumpTo(index: 5);
        },
        tooltip: 'Increment',
        child: const Icon(Icons.gps_not_fixed_outlined),
      ),
      body: SliverViewObserver(
        controller: observerController,
        sliverContexts: () {
          return sliverContextSets.toList();
        },
        child: CustomScrollView(
          controller: scrollController,
          slivers: items.map((vo) {
            return SliverMainAxisGroup(
              slivers: [
                SliverPersistentHeader(
                  pinned: true,
                  delegate: SliverHeaderDelegate.builder(
                    maxHeight: 48,
                    minHeight: 48,
                    builder: (BuildContext context, _, __) {
                      sliverContextSets.add(context);
                      return buildGroup(vo);
                    },
                  ),
                ),
                SliverFixedExtentList(
                  delegate: SliverChildBuilderDelegate(
                    (context, index) {
                      sliverContextSets.add(context);
                      return buildItem(vo.items[index]);
                    },
                    childCount: vo.expand ? vo.items.length : 0,
                  ),
                  itemExtent: 56,
                ),
              ],
            );
          }).toList(),
        ),
      ),
    );
  }

  Widget buildGroup(ItemBean vo) {
    Widget child = const Icon(Icons.arrow_right, size: 12);
    if (vo.expand) child = Transform.rotate(angle: pi / 2, child: child);
    return Column(
      children: [
        Expanded(
          child: GestureDetector(
            onTap: () {
              setState(() => vo.expand = !vo.expand);
            },
            behavior: HitTestBehavior.translucent,
            child: Container(
              alignment: Alignment.centerLeft,
              color: Colors.white,
              child: Row(
                children: [
                  const SizedBox(width: 16),
                  child,
                  const SizedBox(width: 4),
                  Expanded(child: Text(vo.groupName)),
                ],
              ),
            ),
          ),
        ),
        const Divider(indent: 20, endIndent: 20, height: 0),
      ],
    );
  }

  Widget buildItem(String vo) {
    return Column(
      children: [
        Expanded(
          child: Container(
            alignment: Alignment.centerLeft,
            padding: const EdgeInsets.symmetric(horizontal: 32),
            child: Text(vo),
          ),
        ),
        const Divider(indent: 20, endIndent: 20, height: 0),
      ],
    );
  }
}

class ItemBean {
  final String groupName;
  final List<String> items;
  bool expand = false;

  ItemBean({required this.groupName, this.items = const []});

  static List<ItemBean> get groupListData => [
        ItemBean(groupName: '水果', items: [
          '苹果',
          '香蕉',
          '橙子',
          '葡萄',
          '芒果',
          '梨',
          '桃子',
          '草莓',
          '西瓜',
          '柠檬',
          '菠萝',
          '樱桃',
          '蓝莓',
          '猕猴桃',
          '李子',
          '柿子',
          '杏',
          '杨梅',
          '石榴',
          '木瓜'
        ]),
        ItemBean(groupName: '动物', items: [
          '狗',
          '猫',
          '狮子',
          '老虎',
          '大象',
          '熊',
          '鹿',
          '狼',
          '狐狸',
          '猴子',
          '企鹅',
          '熊猫',
          '袋鼠',
          '海豚',
          '鲨鱼',
          '斑马',
          '长颈鹿',
          '鳄鱼',
          '孔雀',
          '乌龟'
        ]),
        ItemBean(groupName: '职业', items: [
          '医生',
          '护士',
          '教师',
          '工程师',
          '程序员',
          '律师',
          '会计',
          '警察',
          '消防员',
          '厨师',
          '司机',
          '飞行员',
          '科学家',
          '记者',
          '设计师',
          '作家',
          '演员',
          '音乐家',
          '画家',
          '摄影师'
        ]),
        ItemBean(groupName: '菜谱', items: [
          '红烧肉',
          '糖醋排骨',
          '宫保鸡丁',
          '麻婆豆腐',
          '鱼香肉丝',
          '酸辣汤',
          '蒜蓉菠菜',
          '回锅肉',
          '水煮鱼',
          '烤鸭',
          '蛋炒饭',
          '蚝油生菜',
          '红烧茄子',
          '西红柿炒鸡蛋',
          '油焖大虾',
          '香菇鸡汤',
          '酸菜鱼',
          '麻辣香锅',
          '铁板牛肉',
          '干煸四季豆'
        ]),
      ];
}

typedef SliverHeaderBuilder = Widget Function(
    BuildContext context, double shrinkOffset, bool overlapsContent);

class SliverHeaderDelegate extends SliverPersistentHeaderDelegate {
  SliverHeaderDelegate({
    required this.maxHeight,
    this.minHeight = 0,
    required Widget child,
  })  : builder = ((a, b, c) => child),
        assert(minHeight <= maxHeight && minHeight >= 0);

  SliverHeaderDelegate.fixedHeight({
    required double height,
    required Widget child,
  })  : builder = ((a, b, c) => child),
        maxHeight = height,
        minHeight = height;

  SliverHeaderDelegate.builder({
    required this.maxHeight,
    this.minHeight = 0,
    required this.builder,
  });

  final double maxHeight;
  final double minHeight;
  final SliverHeaderBuilder builder;

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return SizedBox.expand(
        child: builder(context, shrinkOffset, overlapsContent));
  }

  @override
  double get maxExtent => maxHeight;

  @override
  double get minExtent => minHeight;

  @override
  bool shouldRebuild(SliverHeaderDelegate oldDelegate) {
    return oldDelegate.maxExtent != maxExtent ||
        oldDelegate.minExtent != minExtent ||
        oldDelegate.builder != builder;
  }
}

Try do it

No response