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
421 stars 44 forks source link

[Bug report] #56

Closed shahmirzali49 closed 11 months ago

shahmirzali49 commented 11 months ago

Version

1.16.5

Platforms

dart

Device Model

all phones

flutter info

[✓] Flutter (Channel stable, 3.13.7, on macOS 14.0 23A344 darwin-x64, locale en-TR)
    • Flutter version 3.13.7 on channel stable at /Users/mirzali/development/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 2f708eb839 (13 days ago), 2023-10-09 09:58:08 -0500
    • Engine revision a794cf2681
    • Dart version 3.1.3
    • DevTools version 2.25.0

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
    • Android SDK at /Users/mirzali/Library/Android/sdk
    • Platform android-33, build-tools 33.0.1
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.0)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15A240d
    • CocoaPods version 1.13.0

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] VS Code (version 1.83.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.74.0
[✓] Network resources
    • All expected network resources are available.

• No issues found!

How to reproduce?

alignemnt property not working as excepted in jump or animateTo

alignment: 0.5, I except the widget will be in the center.

observerController.animateTo(
                  sliverContext: _sliverListCtx,
                  index: 10,
                  duration: const Duration(milliseconds: 300),
                  curve: Curves.easeInOut,
                   alignment: 0.5,
                );
import 'package:flutter/material.dart';
import 'package:scrollview_observer/scrollview_observer.dart';
import 'package:scrollview_observer_example/typedefs.dart';

class CustomScrollViewDemoPage extends StatefulWidget {
  const CustomScrollViewDemoPage({Key? key}) : super(key: key);

  @override
  State<CustomScrollViewDemoPage> createState() => _CustomScrollViewDemoPageState();
}

class _CustomScrollViewDemoPageState extends State<CustomScrollViewDemoPage> {
  BuildContext? _sliverListCtx;

  int _hitIndexForCtx1 = 0;

  ScrollController scrollController = ScrollController();

  late SliverObserverController observerController;

  @override
  void initState() {
    super.initState();

    observerController = SliverObserverController(controller: scrollController);

    // Trigger an observation manually
    ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) {
      observerController.dispatchOnceObserve(
        sliverContext: _sliverListCtx!,
      );
    });

    Future.microtask(() {
      observerController.jumpTo(
        index: 20,
        alignment: 0.5,
      );
    });
  }

  @override
  void dispose() {
    observerController.controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("CustomScrollView")),
      body: SliverViewObserver(
        controller: observerController,
        child: CustomScrollView(
          controller: scrollController,
          slivers: [
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (ctx, index) {
                  _sliverListCtx ??= ctx;
                  return Container(
                    height: 300,
                    color: _hitIndexForCtx1 == index ? Colors.red : Colors.black12,
                    child: Center(
                      child: Text(
                        "index -- $index",
                        style: TextStyle(
                          color: _hitIndexForCtx1 == index ? Colors.white : Colors.black,
                        ),
                      ),
                    ),
                  );
                },
                childCount: 30,
              ),
            ),
          ],
        ),
        sliverContexts: () {
          return [
            if (_sliverListCtx != null) _sliverListCtx!,
          ];
        },
      ),
      floatingActionButton: Padding(
        padding: const EdgeInsets.all(15.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            IconButton(
              onPressed: () {
                observerController.animateTo(
                  sliverContext: _sliverListCtx,
                  index: 27,
                  duration: const Duration(milliseconds: 300),
                  curve: Curves.easeInOut,
                  alignment: 0.5,
                );
              },
              icon: const Icon(Icons.ac_unit_outlined),
            ),
          ],
        ),
      ),
    );
  }
}
LinXunFeng commented 11 months ago

It work for me as shown below.

The alignment is for the item, and scrollview_observer will calcuate position from the top of the viewport by default. If you need to scroll the item to the middle of the viewport, you can use the offset callback.

GlobalKey customScrollViewKey = GlobalKey();

CustomScrollView(
  key: customScrollViewKey,
  ...
)

observerController.jumpTo(
  index: 20,
  alignment: 0.5,
  offset: (targetOffset) {
    final customScrollViewRenderObj = (customScrollViewKey.currentContext
        ?.findRenderObject() as RenderBox);
    return customScrollViewRenderObj.size.height * 0.5;
  },
);

Finally, if you need to initialize the index location of the item, it is recommended that you read this document.

2.8、Initial index location

shahmirzali49 commented 11 months ago

I'm using flutter_hooks package, initial index model is not working, can you help me? when I press snow(ac_unit_outlined) icon at the bottom it's working as expected. I think the issue is related context(sliverListCtx).

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

class CustomScrollViewDemoPage extends HookWidget {
  const CustomScrollViewDemoPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final sliverListCtx = useState<BuildContext?>(null);
    final customScrollViewKey = useMemoized(() => GlobalKey());

    final sliverScrollController = useScrollController();
    final observerController = useMemoized(
      () => SliverObserverController(controller: sliverScrollController)
        ..initialIndexModelBlock = () => ObserverIndexPositionModel(
              index: 5,
              sliverContext: sliverListCtx.value,
              offset: (targetOffset) {
                final customScrollViewRenderObj = (customScrollViewKey.currentContext?.findRenderObject() as RenderBox);
                return customScrollViewRenderObj.size.height * 0.5;
              },
              alignment: 0.5,
            ),
    );
    return Scaffold(
      appBar: AppBar(title: const Text("CustomScrollView")),
      body: SliverViewObserver(
        controller: observerController,
        child: CustomScrollView(
          controller: sliverScrollController,
          key: customScrollViewKey,
          slivers: [
            SuperSliverList(
              delegate: SliverChildBuilderDelegate(
                (ctx, index) {
                  WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
                    sliverListCtx.value ??= ctx;
                  });

                  return Container(
                    height: 300,
                    color: Colors.black12,
                    child: Center(
                      child: Text(
                        "index -- $index",
                        style: const TextStyle(
                          color: Colors.black,
                        ),
                      ),
                    ),
                  );
                },
                childCount: 30,
              ),
            ),
          ],
        ),
        sliverContexts: () {
          return [
            if (sliverListCtx.value != null) sliverListCtx.value!,
          ];
        },
        extendedHandleObserve: (context) {
          if (context == sliverListCtx.value) {
            return ObserverCore.handleListObserve(context: context);
          }
          return null;
        },
      ),
      floatingActionButton: Padding(
        padding: const EdgeInsets.all(15.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            IconButton(
              onPressed: () {
                observerController.animateTo(
                  sliverContext: sliverListCtx.value,
                  index: 27,
                  duration: const Duration(milliseconds: 300),
                  curve: Curves.easeInOut,
                  alignment: 0.5,
                   offset: (targetOffset) {
                    final customScrollViewRenderObj =
                        (customScrollViewKey.currentContext?.findRenderObject() as RenderBox);
                    return customScrollViewRenderObj.size.height * 0.5;
                  },
                );
              },
              icon: const Icon(Icons.ac_unit_outlined),
            ),
          ],
        ),
      ),
    );
  }
}

by the way without flutter_hooks it's working mostly.

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

class CustomScrollViewDemoPage extends StatefulWidget {
  const CustomScrollViewDemoPage({Key? key}) : super(key: key);

  @override
  State<CustomScrollViewDemoPage> createState() => _CustomScrollViewDemoPageState();
}

class _CustomScrollViewDemoPageState extends State<CustomScrollViewDemoPage> {
  BuildContext? _sliverListCtx;

  ScrollController scrollController = ScrollController();

  late SliverObserverController observerController;
  GlobalKey customScrollViewKey = GlobalKey();

  @override
  void initState() {
    super.initState();

    observerController = SliverObserverController(controller: scrollController)
      ..initialIndexModel = ObserverIndexPositionModel(
        index: 10,
        alignment: 0.5,
        offset: (targetOffset) {
          final customScrollViewRenderObj = (customScrollViewKey.currentContext?.findRenderObject() as RenderBox);
          return customScrollViewRenderObj.size.height * 0.5;
        },
      );
  }

  @override
  void dispose() {
    observerController.controller?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("CustomScrollView")),
      body: SliverViewObserver(
        controller: observerController,
        child: CustomScrollView(
          controller: scrollController,
          key: customScrollViewKey,
          slivers: [
            SuperSliverList(
              delegate: SliverChildBuilderDelegate(
                (ctx, index) {
                  _sliverListCtx ??= ctx;
                  return Container(
                    height: 300,
                    color: Colors.black12,
                    child: Center(
                      child: Text(
                        "index -- $index",
                        style: const TextStyle(
                          color: Colors.black,
                        ),
                      ),
                    ),
                  );
                },
                childCount: 30,
              ),
            ),
          ],
        ),
        sliverContexts: () {
          return [
            if (_sliverListCtx != null) _sliverListCtx!,
          ];
        },
        extendedHandleObserve: (context) {
          if (context == _sliverListCtx) {
            return ObserverCore.handleListObserve(context: context);
          }
          return null;
        },
      ),
      floatingActionButton: Padding(
        padding: const EdgeInsets.all(15.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            IconButton(
              onPressed: () {
                observerController.animateTo(
                  sliverContext: _sliverListCtx,
                  index: 27,
                  duration: const Duration(milliseconds: 300),
                  curve: Curves.easeInOut,
                  alignment: 0.5,
                  offset: (targetOffset) {
                    final customScrollViewRenderObj =
                        (customScrollViewKey.currentContext?.findRenderObject() as RenderBox);
                    return customScrollViewRenderObj.size.height * 0.5;
                  },
                );
              },
              icon: const Icon(Icons.ac_unit_outlined),
            ),
          ],
        ),
      ),
    );
  }
}
shahmirzali49 commented 11 months ago

when I use .jump() in useEffect(like initstate) it's working but when context update.

 useEffect(() {
      print("sliverListCtx.value ${sliverListCtx.value}");
      observerController.jumpTo(
        index: 10,
        sliverContext: sliverListCtx.value,
        offset: (targetOffset) {
          final customScrollViewRenderObj = (customScrollViewKey.currentContext?.findRenderObject() as RenderBox);
          return customScrollViewRenderObj.size.height * 0.5;
        },
        alignment: 0.5,
      );

      return null;
    }, [sliverListCtx.value]);

look debug console logs. first context is null, but when sliverListCtx update useEffect(initState) is running again. because I added -> [sliverListCtx.value]

flutter: sliverListCtx.value null
flutter: sliverListCtx.value SuperSliverList(delegate: SliverChildBuilderDelegate#4d016(estimated child count: 30), renderObject: _RenderSuperSliverList#1e4ce relayoutBoundary=up1 NEEDS-PAINT)
LinXunFeng commented 11 months ago

The sliverListCtx only plays a recording role, any widget will not be refreshed because of it, so there is no need to use useState.

- final sliverListCtx = useState<BuildContext?>(null);
+ BuildContext? sliverListCtx;

final observerController = useMemoized(
  () => SliverObserverController(controller: sliverScrollController)
    ..initialIndexModelBlock = () {
      return ObserverIndexPositionModel(
        index: 5,
-        sliverContext: sliverListCtx.value,
+        sliverContext: sliverListCtx,
        ...
      );
    },
);

...

SuperSliverList(
  delegate: SliverChildBuilderDelegate(
    (ctx, index) {
+      sliverListCtx ??= ctx;
+
+      // WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+      //   sliverListCtx.value ??= ctx;
+      // });
      ...
  ),
),

...

sliverContexts: () {
  return [
-    if (sliverListCtx.value != null) sliverListCtx.value!,
+    if (sliverListCtx != null) sliverListCtx!,
  ];
},

...

extendedHandleObserve: (context) {
-  if (context == sliverListCtx.value) {
+  if (context == sliverListCtx) {
    return ObserverCore.handleListObserve(context: context);
  }
  return null;
},

... 

observerController.animateTo(
-  sliverContext: sliverListCtx.value,
+  sliverContext: sliverListCtx,
  index: 27,
  ...
);
shahmirzali49 commented 11 months ago

thank you for your help and your time, it's working now. if someone uses flutter_hooks maybe it will help. I used: useRef

final sliverListCtx = useRef<BuildContext?>(null);