Repository: LinXunFeng/flutter_scrollview_observer Branch: main Commit: bd9d306e4f50 Files: 247 Total size: 819.7 KB Directory structure: gitextract_0cpp3x9p/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── assign-issue.yml │ ├── code-analysis.yml │ └── deploy.yml ├── .gitignore ├── .packages ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── README-zh.md ├── README.md ├── analysis_options.yaml ├── example/ │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── android/ │ │ ├── .gitignore │ │ ├── app/ │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ ├── debug/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── example/ │ │ │ │ │ └── example/ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21/ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values/ │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night/ │ │ │ │ └── styles.xml │ │ │ └── profile/ │ │ │ └── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ └── settings.gradle │ ├── ios/ │ │ ├── .gitignore │ │ ├── Flutter/ │ │ │ ├── AppFrameworkInfo.plist │ │ │ ├── Debug.xcconfig │ │ │ └── Release.xcconfig │ │ ├── Podfile │ │ ├── Runner/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ ├── Contents.json │ │ │ │ └── README.md │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── Runner-Bridging-Header.h │ │ ├── Runner.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata/ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── Runner.xcscheme │ │ └── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ ├── lib/ │ │ ├── common/ │ │ │ └── route/ │ │ │ ├── navigation_service.dart │ │ │ ├── route.dart │ │ │ └── router_config.dart │ │ ├── features/ │ │ │ ├── custom_scrollview/ │ │ │ │ ├── custom_scrollview_demo/ │ │ │ │ │ ├── custom_scrollview_center_demo_page.dart │ │ │ │ │ ├── custom_scrollview_demo_page.dart │ │ │ │ │ └── multi_sliver_demo_page.dart │ │ │ │ └── sliver_appbar_demo/ │ │ │ │ └── sliver_appbar_demo_page.dart │ │ │ ├── gridview/ │ │ │ │ ├── gridview_ctx_demo/ │ │ │ │ │ └── gridview_ctx_demo_page.dart │ │ │ │ ├── gridview_custom_demo/ │ │ │ │ │ └── gridview_custom_demo_page.dart │ │ │ │ ├── gridview_demo/ │ │ │ │ │ └── gridview_demo_page.dart │ │ │ │ ├── gridview_fixed_height_demo/ │ │ │ │ │ └── gridview_fixed_height_demo_page.dart │ │ │ │ ├── horizontal_gridview_demo/ │ │ │ │ │ └── horizontal_gridview_demo_page.dart │ │ │ │ └── sliver_grid_demo/ │ │ │ │ └── sliver_grid_demo_page.dart │ │ │ ├── home/ │ │ │ │ └── home_page.dart │ │ │ ├── listview/ │ │ │ │ ├── horizontal_listview_demo/ │ │ │ │ │ └── horizontal_listview_page.dart │ │ │ │ ├── infinite_listview_demo/ │ │ │ │ │ └── infinite_listview_page.dart │ │ │ │ ├── listview_ctx_demo/ │ │ │ │ │ └── listview_ctx_demo_page.dart │ │ │ │ ├── listview_custom_demo/ │ │ │ │ │ └── listview_custom_demo_page.dart │ │ │ │ ├── listview_demo/ │ │ │ │ │ └── listview_demo_page.dart │ │ │ │ ├── listview_dynamic_offset/ │ │ │ │ │ └── listview_dynamic_offset_page.dart │ │ │ │ ├── listview_fixed_height_demo/ │ │ │ │ │ └── listview_fixed_height_demo_page.dart │ │ │ │ └── sliver_list_demo/ │ │ │ │ └── sliver_list_demo_page.dart │ │ │ ├── nested_scrollview/ │ │ │ │ ├── nested_scrollview_demo/ │ │ │ │ │ └── nested_scrollview_demo_page.dart │ │ │ │ └── nested_scrollview_tab_bar_view_demo/ │ │ │ │ ├── header/ │ │ │ │ │ └── nested_scrollview_tab_bar_view_demo_header.dart │ │ │ │ ├── logic/ │ │ │ │ │ ├── nested_scrollview_tab_bar_view_demo_logic.dart │ │ │ │ │ ├── nested_scrollview_tab_bar_view_demo_logic_floating_action_btn.dart │ │ │ │ │ ├── nested_scrollview_tab_bar_view_demo_logic_observer.dart │ │ │ │ │ ├── nested_scrollview_tab_bar_view_demo_logic_scroll_type_switch.dart │ │ │ │ │ └── nested_scrollview_tab_bar_view_demo_logic_tab_bar.dart │ │ │ │ ├── page/ │ │ │ │ │ └── nested_scrollview_tab_bar_view_demo_page.dart │ │ │ │ ├── state/ │ │ │ │ │ └── nested_scrollview_tab_bar_view_demo_state.dart │ │ │ │ └── widget/ │ │ │ │ ├── nested_scrollview_tab_bar_view_demo_floating_action_btn.dart │ │ │ │ ├── nested_scrollview_tab_bar_view_demo_header_list_sliver.dart │ │ │ │ ├── nested_scrollview_tab_bar_view_demo_scroll_type_switch.dart │ │ │ │ ├── nested_scrollview_tab_bar_view_demo_tab1_view.dart │ │ │ │ ├── nested_scrollview_tab_bar_view_demo_tab2_view.dart │ │ │ │ ├── nested_scrollview_tab_bar_view_demo_tab3_view.dart │ │ │ │ └── nested_scrollview_tab_bar_view_demo_tabbar.dart │ │ │ ├── pageview/ │ │ │ │ └── pageview_demo/ │ │ │ │ ├── pageview_demo_page.dart │ │ │ │ ├── pageview_parallax_item_listener_page.dart │ │ │ │ └── pageview_parallax_page.dart │ │ │ └── scene/ │ │ │ ├── anchor_demo/ │ │ │ │ ├── anchor_page.dart │ │ │ │ └── anchor_waterfall_page.dart │ │ │ ├── azlist_demo/ │ │ │ │ ├── azlist_cursor.dart │ │ │ │ ├── azlist_index_bar.dart │ │ │ │ ├── azlist_item_view.dart │ │ │ │ ├── azlist_model.dart │ │ │ │ └── azlist_page.dart │ │ │ ├── chat_demo/ │ │ │ │ ├── helper/ │ │ │ │ │ └── chat_data_helper.dart │ │ │ │ ├── model/ │ │ │ │ │ └── chat_model.dart │ │ │ │ ├── page/ │ │ │ │ │ ├── chat_gpt_page.dart │ │ │ │ │ └── chat_page.dart │ │ │ │ └── widget/ │ │ │ │ ├── chat_item_widget.dart │ │ │ │ └── chat_unread_tip_view.dart │ │ │ ├── detail/ │ │ │ │ ├── header/ │ │ │ │ │ └── detail_header.dart │ │ │ │ ├── logic/ │ │ │ │ │ ├── detail_logic.dart │ │ │ │ │ ├── detail_logic_config.dart │ │ │ │ │ ├── detail_logic_list_view.dart │ │ │ │ │ └── detail_logic_nav_bar.dart │ │ │ │ ├── model/ │ │ │ │ │ └── detail_nav_bar_tab_model.dart │ │ │ │ ├── page/ │ │ │ │ │ └── detail_page.dart │ │ │ │ ├── state/ │ │ │ │ │ ├── detail_state.dart │ │ │ │ │ ├── detail_state_config.dart │ │ │ │ │ ├── detail_state_list_view.dart │ │ │ │ │ └── detail_state_nav_bar.dart │ │ │ │ └── widget/ │ │ │ │ ├── detail_config_view.dart │ │ │ │ ├── detail_list_item_wrapper.dart │ │ │ │ ├── detail_list_view.dart │ │ │ │ ├── detail_nav_bar.dart │ │ │ │ └── list_item/ │ │ │ │ ├── detail_list_module1.dart │ │ │ │ ├── detail_list_module2.dart │ │ │ │ ├── detail_list_module3.dart │ │ │ │ ├── detail_list_module4.dart │ │ │ │ ├── detail_list_module5.dart │ │ │ │ ├── detail_list_module6.dart │ │ │ │ ├── detail_list_module7.dart │ │ │ │ └── detail_list_module8.dart │ │ │ ├── expandable_carousel_slider_demo/ │ │ │ │ └── expandable_carousel_slider_demo.dart │ │ │ ├── image_tab_demo/ │ │ │ │ └── image_tab_page.dart │ │ │ ├── scrollview_form_demo/ │ │ │ │ └── scrollview_form_demo_page.dart │ │ │ ├── video_auto_play_list/ │ │ │ │ ├── video_list_auto_play_page.dart │ │ │ │ └── widgets/ │ │ │ │ └── video_widget.dart │ │ │ ├── visibility_demo/ │ │ │ │ ├── mixin/ │ │ │ │ │ └── visibility_exposure_mixin.dart │ │ │ │ └── page/ │ │ │ │ ├── visibility_listview_page.dart │ │ │ │ └── visibility_scrollview_page.dart │ │ │ ├── waterfall_flow_demo/ │ │ │ │ ├── waterfall_flow_grid_item_view.dart │ │ │ │ ├── waterfall_flow_page.dart │ │ │ │ ├── waterfall_flow_swipe_view.dart │ │ │ │ └── waterfall_flow_type.dart │ │ │ └── waterfall_flow_fixed_height_demo/ │ │ │ └── waterfall_flow_fixed_height_page.dart │ │ ├── main.dart │ │ ├── typedefs.dart │ │ ├── utils/ │ │ │ ├── keyboard.dart │ │ │ ├── random.dart │ │ │ └── snackbar.dart │ │ └── widgets/ │ │ ├── animation.dart │ │ └── sliver.dart │ ├── linux/ │ │ └── flutter/ │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ ├── macos/ │ │ ├── .gitignore │ │ ├── Flutter/ │ │ │ ├── Flutter-Debug.xcconfig │ │ │ ├── Flutter-Release.xcconfig │ │ │ └── GeneratedPluginRegistrant.swift │ │ ├── Podfile │ │ ├── Runner/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── MainMenu.xib │ │ │ ├── Configs/ │ │ │ │ ├── AppInfo.xcconfig │ │ │ │ ├── Debug.xcconfig │ │ │ │ ├── Release.xcconfig │ │ │ │ └── Warnings.xcconfig │ │ │ ├── DebugProfile.entitlements │ │ │ ├── Info.plist │ │ │ ├── MainFlutterWindow.swift │ │ │ └── Release.entitlements │ │ ├── Runner.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ └── xcshareddata/ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── Runner.xcscheme │ │ └── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ ├── pubspec.yaml │ ├── test/ │ │ └── widget_test.dart │ ├── web/ │ │ ├── index.html │ │ └── manifest.json │ └── windows/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter/ │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ └── runner/ │ ├── CMakeLists.txt │ ├── Runner.rc │ ├── flutter_window.cpp │ ├── flutter_window.h │ ├── main.cpp │ ├── resource.h │ ├── runner.exe.manifest │ ├── utils.cpp │ ├── utils.h │ ├── win32_window.cpp │ └── win32_window.h ├── lib/ │ ├── scrollview_observer.dart │ └── src/ │ ├── common/ │ │ ├── models/ │ │ │ ├── observe_displaying_child_model.dart │ │ │ ├── observe_displaying_child_model_mixin.dart │ │ │ ├── observe_find_child_model.dart │ │ │ ├── observe_model.dart │ │ │ ├── observe_scroll_child_model.dart │ │ │ ├── observe_scroll_to_index_result_model.dart │ │ │ ├── observer_handle_contexts_result_model.dart │ │ │ └── observer_index_position_model.dart │ │ ├── observer_controller.dart │ │ ├── observer_listener.dart │ │ ├── observer_notification_result.dart │ │ ├── observer_typedef.dart │ │ ├── observer_widget.dart │ │ ├── observer_widget_scope.dart │ │ ├── observer_widget_tag_manager.dart │ │ └── typedefs.dart │ ├── gridview/ │ │ ├── grid_observer_controller.dart │ │ ├── grid_observer_notification_result.dart │ │ ├── grid_observer_view.dart │ │ └── models/ │ │ ├── gridview_observe_displaying_child_model.dart │ │ └── gridview_observe_model.dart │ ├── listview/ │ │ ├── list_observer_controller.dart │ │ ├── list_observer_notification_result.dart │ │ ├── list_observer_view.dart │ │ └── models/ │ │ ├── listview_observe_displaying_child_model.dart │ │ └── listview_observe_model.dart │ ├── notification.dart │ ├── observer_core.dart │ ├── sliver/ │ │ ├── models/ │ │ │ ├── sliver_observer_observe_result_model.dart │ │ │ ├── sliver_viewport_observe_displaying_child_model.dart │ │ │ └── sliver_viewport_observe_model.dart │ │ ├── sliver_observer_controller.dart │ │ ├── sliver_observer_listener.dart │ │ ├── sliver_observer_notification_result.dart │ │ └── sliver_observer_view.dart │ └── utils/ │ ├── observer_utils.dart │ └── src/ │ ├── chat/ │ │ ├── chat_observer_scroll_physics.dart │ │ ├── chat_observer_scroll_physics_mixin.dart │ │ ├── chat_scroll_observer.dart │ │ ├── chat_scroll_observer_model.dart │ │ └── chat_scroll_observer_typedefs.dart │ ├── extends.dart │ ├── log.dart │ ├── nested_scroll_util.dart │ ├── observer_utils.dart │ └── slivers.dart ├── listview_observer.iml ├── pubspec.yaml └── test/ ├── chat_scroll_observer_test.dart ├── grid_observer_test.dart ├── list_observer_test.dart ├── observer_utils_test.dart └── sliver_observer_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] # patreon: # Replace with a single Patreon username # open_collective: # Replace with a single Open Collective username # ko_fi: # Replace with a single Ko-fi username # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry # liberapay: # Replace with a single Liberapay username # issuehunt: # Replace with a single IssueHunt username # otechie: # Replace with a single Otechie username # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] github: LinXunFeng ko_fi: linxunfeng ================================================ FILE: .github/workflows/assign-issue.yml ================================================ name: Issue assignment on: issues: types: [opened] jobs: auto-assign: runs-on: ubuntu-latest steps: - name: 'Auto-assign issue' uses: pozil/auto-assign-issue@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} assignees: LinXunFeng ================================================ FILE: .github/workflows/code-analysis.yml ================================================ name: Code Analysis on: workflow_dispatch: push: branches: - main paths-ignore: - '**.md' pull_request: branches: - main paths-ignore: - '**.md' jobs: code-analysis: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: 'stable' - name: Prepare dependencies run: | flutter --version flutter pub get - name: Check Dart code formatting run: | dart format . -o none --set-exit-if-changed - name: Analyze Dart code run: | flutter analyze . - name: Generate dartdoc run: | dart pub global activate dartdoc dart pub global run dartdoc . test: needs: [code-analysis] runs-on: ubuntu-latest strategy: matrix: flutter-version: [''] steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: 'stable' flutter-version: ${{ matrix.flutter-version }} - name: Prepare dependencies run: | flutter --version flutter pub get - name: Test run: flutter test ================================================ FILE: .github/workflows/deploy.yml ================================================ name: Deploy to GitHub Pages on: push: branches: - main # - develop jobs: build: name: Build Web env: my_secret: ${{secrets.commit_secret}} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: # flutter-version: '3.16.9' channel: 'stable' - name: Flutter build web working-directory: ./example run: | flutter config --enable-web flutter pub get flutter build web --release --base-href /flutter_scrollview_observer/ - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./example/build/web force_orphan: true user_name: 'github-ci[bot]' user_email: 'github-actions[bot]@users.noreply.github.com' commit_message: 'Publish to gh-pages' ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related # *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. .vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies # .packages .pub-cache/ .pub/ /build/ # Web related lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: .packages ================================================ # This file is deprecated. Tools should instead consume # `.dart_tool/package_config.json`. # # For more info see: https://dart.dev/go/dot-packages-deprecation # # Generated by pub on 2023-03-02 22:51:15.844107. async:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/async-2.9.0/lib/ boolean_selector:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/boolean_selector-2.1.0/lib/ characters:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/characters-1.2.0/lib/ charcode:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/ clock:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/clock-1.1.0/lib/ collection:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/collection-1.16.0/lib/ fake_async:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/fake_async-1.3.0/lib/ flutter:file:///Users/lxf/fvm/versions/3.1.0/packages/flutter/lib/ flutter_lints:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/flutter_lints-1.0.4/lib/ flutter_test:file:///Users/lxf/fvm/versions/3.1.0/packages/flutter_test/lib/ lints:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/lints-1.0.1/lib/ matcher:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/matcher-0.12.11/lib/ material_color_utilities:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/material_color_utilities-0.1.4/lib/ meta:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/ path:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/path-1.8.1/lib/ sky_engine:file:///Users/lxf/fvm/versions/3.1.0/bin/cache/pkg/sky_engine/lib/ source_span:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.2/lib/ stack_trace:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.10.0/lib/ stream_channel:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/stream_channel-2.1.0/lib/ string_scanner:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/ term_glyph:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/ test_api:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/test_api-0.4.9/lib/ vector_math:file:///Users/lxf/.pub-cache/hosted/pub.dartlang.org/vector_math-2.1.2/lib/ scrollview_observer:lib/ ================================================ FILE: CHANGELOG.md ================================================ ## 1.26.3 - ObserverWidget - Fix incorrect tag comparison in `_checkTagChange` by @LinXunFeng in [#143](https://github.com/fluttercandies/flutter_scrollview_observer/issues/143). - ObserverController - Add missing type annotations by @LinXunFeng. ## 1.26.2 - ObserverUtils - Refine the logic of `calcAnchorTabIndex` method by @LinXunFeng. ## 1.26.1 - ChatScrollObserver - Refine the logic of `standby` by @LinXunFeng. ## 1.26.0 - NestedScrollUtil - Add await support for `animateTo` and `jumpTo` methods by @LinXunFeng. ## 1.25.2 - ObserverWidget - Fix "ObserverWidgetState was used after being disposed" by @LinXunFeng in [#120](https://github.com/fluttercandies/flutter_scrollview_observer/issues/120) and [#123](https://github.com/fluttercandies/flutter_scrollview_observer/issues/123). ## 1.25.1 - ObserverWidget - Fix observation result inaccurate by @LinXunFeng in [#113](https://github.com/fluttercandies/flutter_scrollview_observer/issues/113). ## 1.25.0 - ChatScrollObserver - Add `customAdjustPosition`. ## 1.24.0 - ObserveModel - Add `displayingChildModelMap`. - ChatScrollObserver - Add `customAdjustPositionDelta`. ## 1.23.0 - ObserverWidget - Make `ObserverWidget` listenable. ## 1.22.0 - ObserverWidget - Add `scrollNotificationPredicate`. ## 1.21.2 - ObserverWidget - Fix web support by @Ahmadre in [#91](https://github.com/LinXunFeng/flutter_scrollview_observer/pull/91). ## 1.21.1 - ObserverCore - Improve the logic of `handleListObserve` and `handleGridObserve`. ## 1.21.0 - ObserverController - Add `onPrepareScrollToIndex` for `jumpTo` and `animateTo`. - NestedScrollUtil - Add `jumpTo` and `animateTo` methods. ## 1.20.0 - ObserverController - Add `observeIntervalForScrolling` to set the minimum amount of time to wait for firing observe callback during scrolling. - ObserverUtils - Improve the logic of `isDisplayingSliverInViewport` method. - ScrollViewOnceObserveNotificationResult - Add `observeViewportResultModel`. - ChatScrollObserver - Add `ChatScrollObserverRefIndexType`. - Add `refIndexType` to specify the role of `refItemIndex` and `refItemIndex`. - Slivers - Add `SliverObserveContext`. - ObserveDisplayingChildModelMixin - Add `visibleFraction` and `visibleMainAxisSize`. ## 1.19.1 - ListViewObserver - Support `SliverVariedExtentList` in [74](https://github.com/fluttercandies/flutter_scrollview_observer/issues/74). - ChatScrollObserver - Safely obtain the `constraints` of RenderSliver. - ObserverController - Adapt to scenes where `CustomScrollView` specifies `center`. ## 1.19.0 - SliverViewObserver - Add `customOverlap` property. - NestedScrollUtil - To better support `NestedScrollView`. - ObserverCore - Improve the judgment logic of whether the sliver is visible. - ObserverController - Adjust the controller to be modifiable. ## 1.18.2 - ChatScrollObserver - Fix keeping position not working by @LinXunFeng in [#64](https://github.com/fluttercandies/flutter_scrollview_observer/issues/64) ## 1.18.1 - Add a check to determine whether the BuildContext is mounted by @LinXunFeng in [#62](https://github.com/fluttercandies/flutter_scrollview_observer/issues/62) ## 1.18.0 - ObserverController - Add some scrolling task notifications extending from `ObserverScrollNotification`. - The `jumpTo` method and `animateTo` method support `await`. - Add `isForbidObserveCallback` property. - Add `isForbidObserveViewportCallback` property. ## 1.17.0 - ObserverController - The parameter `isFixedHeight` in the `jumpTo` method and `animateTo` method supports GridView and supports ScrollView built by third-party package by @percival888 in [#52](https://github.com/fluttercandies/flutter_scrollview_observer/pull/52). ## 1.16.5 - ObserverWidget - Improve the processing logic of scroll notification when scrolling with the mouse wheel is not smooth by @qiangjindong in [#48](https://github.com/LinXunFeng/flutter_scrollview_observer/pull/48). - ChatScrollObserver - Update `isShrinkWrap` once during initialization by @LinXunFeng. [#47](https://github.com/LinXunFeng/flutter_scrollview_observer/issues/47) - ObserverController - Fix unable to jump when sliver is too far away and has no any child by @LinXunFeng. [#45](https://github.com/LinXunFeng/flutter_scrollview_observer/issues/45) - Fix continuous page turning due to incorrect index by @LinXunFeng. ## 1.16.4 - Supplement missing type conversion adjustments. ## 1.16.3 - Observation Model - Add viewport property to observation model. - Correct the calculation logic of some values to adapt to the scene of multiple slivers. ## 1.16.2 - ObserverWidget - Adjust the conversion type of `viewport.offset` to `ScrollPosition` by @LiuDongCai in [#41](https://github.com/LinXunFeng/flutter_scrollview_observer/pull/41). - ObserveDisplayingChildModelMixin - Adapt `displayPercentage` to scenes with SliverPersistentHeader by @percival888 in [#43](https://github.com/LinXunFeng/flutter_scrollview_observer/pull/43). - Refine the logic of `calculateDisplayPercentage` method by @LinXunFeng. ## 1.16.1 - Compatible with flutter 3.13.0 ## 1.16.0 - ObserverController - `dispatchOnceObserve` method supports directly getting observation result. ## 1.15.0 - Slivers - Add `SliverObserveContextToBoxAdapter`. ## 1.14.2 - ObserverWidget - Safe to use context. [#35](https://github.com/LinXunFeng/flutter_scrollview_observer/issues/35). ## 1.14.1 - ChatScrollObserver - Improve the logic of the conversion type. ## 1.14.0 - ChatScrollObserver - Support for keeping position of generative messages (eg: ChatGPT) ## 1.13.2 - ObserverWidget - Fix getting bad observation result on web. Thanks to @rmasarovic for the test in [#31](https://github.com/LinXunFeng/flutter_scrollview_observer/issues/31) ## 1.13.1 - ObserverCore - Fix no getting all child widgets those are displayed when there are separators in `ListView`. [#31](https://github.com/LinXunFeng/flutter_scrollview_observer/issues/31) - ObserverUtils - Safely call `findRenderObject` method. ## 1.13.0 - ObserverUtils - The `calcAnchorTabIndex` method supports GridView. - ObserverCore - Refine the logic of `handleListObserve` method and `handleGridObserve` method. ## 1.12.0 - ObserverWidget - Support custom observation object and observation logic. - Refine the logic for finding the first sliver in viewport. ## 1.11.0 - ChatScrollObserver - Support inserting multiple messages at once. - ObserverWidget - `GridViewObserver` is compatible with waterfall flow. - `SliverViewObserver` supports observation of viewport. ## 1.10.1 - ObserverController - fix: targetOffset calculate may be negative by @zeqinjie in [#21](https://github.com/LinXunFeng/flutter_scrollview_observer/pull/21). ## 1.10.0 - ObserverController - Improve `[_calculateTargetLayoutOffset]` logic. - The `jumpTo` method and `animateTo` method both add a parameter `[padding]`. - ObserverIndexPositionModel - Add property `[padding]`. ## 1.9.2 - ObserverWidget - Catch the exception thrown by getting size. ## 1.9.1 - ObserverController - Modify offset calculation logic in method `[_calculateTargetLayoutOffset]`. ## 1.9.0 - ObserverWidget - Add property `[autoTriggerObserveTypes]` and property `[triggerOnObserveType]`. - ObserverController - Method `[dispatchOnceObserve]` adds parameter `[isForce]`. ## 1.8.0 - Scrolling to the specified index location - Supports initializing the index position of the scrollView. - Deprecated `[clearIndexOffsetCache]`, please use `[clearScrollIndexCache]` instead. ## 1.7.0 - ChatScrollObserver - Add the property `[fixedPositionOffset]`. - Deprecated `[ChatObserverClampinScrollPhysics]`, please use `[ChatObserverClampingScrollPhysics]` instead. ## 1.6.2 - Fix lib not working when `itemExtent` is set in `ListView`. ## 1.6.1 - Fix lib not working when `shrinkWrap` is `true` in scrollView. ## 1.6.0 - ChatScrollObserver - Add `onHandlePositionCallback`. ## 1.5.1 - Fix scrollView being stuck when child widget get `[size]`. ## 1.5.0 - ChatScrollObserver - Quickly implement the chat session page effect. - Scrolling to the specified index location - Add the property `[cacheJumpIndexOffset]`. ## 1.4.0 - Scrolling to the specified index location - New `alignment` parameter in the `jumpTo` and `animateTo` methods. - Fixed a bug that caused scrolling to the first child to jitter when using `offset` parameter. ## 1.3.0 - Scrolling to the specified index location supports the `SliverPersistentHeader`. - Add `ObserverUtils` - Method `calcPersistentHeaderExtent`: Calculate current extent of `RenderSliverPersistentHeader`. - Method `calcAnchorTabIndex`: Calculate the anchor tab index. ## 1.2.0 - The `jumpTo` and `animateTo` methods add an `isFixedHeight` parameter to optimize performance when the child widget is of fixed height - Add the properties `[leadingMarginToViewport]` and `[trailingMarginToViewport]` - Support mixing usage of `SliverList` and `SliverGrid` ## 1.1.0 - Supports scrolling to the specified index location ## 1.0.1 - Delete useless code ## 1.0.0 - Implements a way to use without sliver [BuildContext] - Change [onObserve] to [onObserveAll], and add a new [onObserve] callback to listen for changes in the child widget display of the first sliver - Add [ObserverController] ## 0.1.0 - Support `GridView` - Support the horizontal ## 0.0.1 - Initial release ================================================ FILE: CODEOWNERS ================================================ * @LinXunFeng ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 LinXunFeng Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README-zh.md ================================================ ![](https://github.com/LinXunFeng/flutter_assets/raw/main/flutter_scrollview_observer/banner.png) # Flutter ScrollView Observer [![author](https://img.shields.io/badge/author-LinXunFeng-blue.svg?style=flat-square&logo=Iconify)](https://github.com/LinXunFeng/) [![pub](https://img.shields.io/pub/v/scrollview_observer?&style=flat-square&label=pub&logo=dart)](https://pub.dev/packages/scrollview_observer) [![stars](https://img.shields.io/github/stars/fluttercandies/flutter_scrollview_observer?style=flat-square&logo=github)](https://github.com/fluttercandies/flutter_scrollview_observer) Language: 中文 | [English](https://github.com/fluttercandies/flutter_scrollview_observer) 这是一个可用于监听滚动视图中正在显示的子部件的组件库。 ## ☕ 请我喝一杯咖啡 [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/T6T4JKVRP) [![wechat](https://img.shields.io/static/v1?label=WeChat&message=微信收款码&color=brightgreen&style=for-the-badge&logo=WeChat)](https://cdn.jsdelivr.net/gh/FullStackAction/PicBed@resource20220417121922/image/202303181116760.jpeg) 微信技术交流群请看: [【微信群说明】](https://mp.weixin.qq.com/s/JBbMstn0qW6M71hh-BRKzw) ## 📖 文章 - [Flutter - 获取ListView当前正在显示的Widget信息](https://mp.weixin.qq.com/s/cN3qeinBPlo5rtEpoQBVVA) | [备用链接](https://juejin.cn/post/7103058155692621837) - [Flutter - 列表滚动定位超强辅助库,墙裂推荐!🔥](https://mp.weixin.qq.com/s/fplqfBpXwvx6mEO6vflkww) | [备用链接](https://juejin.cn/post/7129888644290068487) - [Flutter - 快速实现聊天会话列表的效果,完美💯](https://mp.weixin.qq.com/s/xNiGuSLcJtDAiLoHuGWp6A) | [备用链接](https://juejin.cn/post/7152307272436154405) - [Flutter - 船新升级😱支持观察第三方构建的滚动视图💪](https://mp.weixin.qq.com/s/FMXPyT-lX8YOXVmbLCsVUA) | [备用链接](https://juejin.cn/post/7240751116702269477) - [Flutter - 瀑布流交替播放视频 🎞](https://mp.weixin.qq.com/s/miP5CfKtcRhFGr08ot5wOg) | [备用链接](https://juejin.cn/post/7243240589293142077) - [Flutter - IM保持消息位置大升级(支持ChatGPT生成式消息) 🤖](https://mp.weixin.qq.com/s/Y3EN9ZpLb6HLke2vkw0Zwg) | [备用链接](https://juejin.cn/post/7245753944180523067) - [Flutter - 滚动视图中的表单防遮挡 🗒](https://mp.weixin.qq.com/s/iaHyYMjZSPBggLw2yZv8dQ) | [备用链接](https://juejin.cn/spost/7266455050632921107) - [Flutter - 秒杀1/2曝光统计 📊](https://mp.weixin.qq.com/s/gNFX4Au4esftgTPXHvB4LQ) | [备用链接](https://juejin.cn/post/7271248528998121512) - [Flutter - 如何快速搓一个微信通讯录列表(azlist) 📓](https://mp.weixin.qq.com/s/1bmYSvtOYX83DLncvnBjqA) | [备用链接](https://juejin.cn/post/7294884963631497254) - [Flutter - 支持观察NestedScrollView,兼容性更强 😈](https://mp.weixin.qq.com/s/1dsmRg8q2VJ6HzasLgoVpA) | [备用链接](https://juejin.cn/post/7388444606456840211) - [Flutter - 轻松实现PageView卡片偏移效果](https://mp.weixin.qq.com/s/Q8zk89bgr_8bgWQ4F86VUQ) | [备用链接](https://juejin.cn/post/7411516362916216859) - [Flutter - 轻松搞定炫酷视差(Parallax)效果](https://mp.weixin.qq.com/s/Fi-X2eJRWj17sqCcVqbPRQ) | [备用链接](https://juejin.cn/post/7416655730214699017) - [Flutter - 详情页 TabBar 与模块联动?秒了!](https://mp.weixin.qq.com/s/uLRLzxS4IqmCq0gewTS8kQ) | [备用链接](https://juejin.cn/post/7538868042961911817) - [Flutter - 详情页初始锚点与优化](https://mp.weixin.qq.com/s/hRPc_eHjl0OSKFKfmj6q_g) | [备用链接](https://juejin.cn/post/7541801054188109850) ## 🔨 功能点 > 不需要改变你当前所使用视图,只需要在视图外包裹一层即可实现如下功能点 - [x] 监听滚动视图中正在显示的子部件 - [x] 支持滚动到指定下标位置 - [x] 快速实现聊天会话列表的效果 - [x] 支持在插入或更新消息时保持IM消息位置,避免抖动 ## 🎀 支持 - [x] `PageView` - [x] `ListView` - [x] `SliverList` - [x] `GridView` - [x] `SliverGrid` - [x] 支持 `SliverPersistentHeader`,`SliverList` 和 `SliverGrid` 混合使用 - [x] `NestedScrollView` - [x] 由第三方库构建的 `ScrollView` ## 🕹 预览 - 🖥 [在线预览](https://fluttercandies.github.io/flutter_scrollview_observer/) - 🏞 [示例图片](https://github.com/fluttercandies/flutter_scrollview_observer/wiki/Example) ## 📦 安装 在你的 `pubspec.yaml` 文件中添加 `scrollview_observer` 依赖: ```yaml dependencies: scrollview_observer: latest_version ``` 在需要使用的地方导入 `scrollview_observer` : ```dart import 'package:scrollview_observer/scrollview_observer.dart'; ``` ## 📚 指南 - [Wiki首页](https://github.com/fluttercandies/flutter_scrollview_observer/wiki/%E9%A6%96%E9%A1%B5) - [1、监听滚动视图中正在显示的子部件](https://github.com/fluttercandies/flutter_scrollview_observer/wiki/1%E3%80%81%E7%9B%91%E5%90%AC%E6%BB%9A%E5%8A%A8%E8%A7%86%E5%9B%BE%E4%B8%AD%E6%AD%A3%E5%9C%A8%E6%98%BE%E7%A4%BA%E7%9A%84%E5%AD%90%E9%83%A8%E4%BB%B6) - [2、滚动到指定下标位置](https://github.com/fluttercandies/flutter_scrollview_observer/wiki/2%E3%80%81%E6%BB%9A%E5%8A%A8%E5%88%B0%E6%8C%87%E5%AE%9A%E4%B8%8B%E6%A0%87%E4%BD%8D%E7%BD%AE) - [3、聊天会话](https://github.com/fluttercandies/flutter_scrollview_observer/wiki/3%E3%80%81%E8%81%8A%E5%A4%A9%E4%BC%9A%E8%AF%9D) ## 🖨 关于我 - GitHub: [https://github.com/LinXunFeng](https://github.com/LinXunFeng) - Email: [linxunfeng@yeah.net](mailto:linxunfeng@yeah.net) - Blogs: - 全栈行动: [https://fullstackaction.com](https://fullstackaction.com) - 掘金: [https://juejin.cn/user/1820446984512392](https://juejin.cn/user/1820446984512392) ================================================ FILE: README.md ================================================ ![](https://github.com/LinXunFeng/flutter_assets/raw/main/flutter_scrollview_observer/banner.png) # Flutter ScrollView Observer [![author](https://img.shields.io/badge/author-LinXunFeng-blue.svg?style=flat-square&logo=Iconify)](https://github.com/LinXunFeng/) [![pub](https://img.shields.io/pub/v/scrollview_observer?&style=flat-square&label=pub&logo=dart)](https://pub.dev/packages/scrollview_observer) [![stars](https://img.shields.io/github/stars/fluttercandies/flutter_scrollview_observer?style=flat-square&logo=github)](https://github.com/fluttercandies/flutter_scrollview_observer) Language: English | [中文](https://github.com/fluttercandies/flutter_scrollview_observer/blob/main/README-zh.md) This is a library of widget that can be used to listen for child widgets those are being displayed in the scroll view. ## ☕ Support me [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/T6T4JKVRP) [![wechat](https://img.shields.io/static/v1?label=WeChat&message=WeChat Pay&color=brightgreen&style=for-the-badge&logo=WeChat)](https://cdn.jsdelivr.net/gh/FullStackAction/PicBed@resource20220417121922/image/202303181116760.jpeg) Chat: [Join WeChat group](https://mp.weixin.qq.com/s/JBbMstn0qW6M71hh-BRKzw) ## 📖 Article - [Flutter - Getting the items information those are currently displaying in ScrollView](https://medium.com/@linxunfeng/flutter-gets-the-items-information-those-are-currently-displaying-in-scrollview-6cb4f27f2536) | [WeChat](https://mp.weixin.qq.com/s/cN3qeinBPlo5rtEpoQBVVA) | [JueJin](https://juejin.cn/post/7103058155692621837) - [Flutter - Scrolling to a specific item in the ScrollView!🔥](https://medium.com/@linxunfeng/flutter-scrolling-to-a-specific-item-in-the-scrollview-b89d3f10eee0) | [WeChat](https://mp.weixin.qq.com/s/fplqfBpXwvx6mEO6vflkww) | [JueJin](https://juejin.cn/post/7129888644290068487) - [Flutter - Quickly implement the effect of the chat session list, perfect 💯](https://medium.com/@linxunfeng/flutter-quickly-implement-the-effect-of-the-chat-session-list-perfect-68c8acea4568) | [WeChat](https://mp.weixin.qq.com/s/xNiGuSLcJtDAiLoHuGWp6A) | [JueJin](https://juejin.cn/post/7152307272436154405) - [Flutter - New upgrade😱Supports observing ScrollView built by third package💪](https://medium.com/@linxunfeng/flutter-new-upgrade-supports-observing-scrollview-built-by-third-package-2e5af3d84c64) | [WeChat](https://mp.weixin.qq.com/s/FMXPyT-lX8YOXVmbLCsVUA) | [JueJin](https://juejin.cn/post/7240751116702269477) - [Flutter - Play alternately waterfall flow video 🎞](https://medium.com/@linxunfeng/flutter-play-alternately-waterfall-flow-video-015279bc8e14) | [WeChat](https://mp.weixin.qq.com/s/miP5CfKtcRhFGr08ot5wOg) | [JueJin](https://juejin.cn/post/7243240589293142077) - [Flutter - Keep IM message position greatly upgraded (supports generative messages like ChatGPT) 🤖](https://medium.com/@linxunfeng/flutter-keep-im-message-position-greatly-upgraded-supports-generative-messages-like-chatgpt-2b199a7b14e7) | [WeChat](https://mp.weixin.qq.com/s/Y3EN9ZpLb6HLke2vkw0Zwg) | [JueJin](https://juejin.cn/post/7245753944180523067) - [Flutter - Anti-occlusion of form in ScrollView 🗒](https://medium.com/@linxunfeng/flutter-anti-occlusion-of-form-in-scrollview-ad8bde15e18d) | [WeChat](https://mp.weixin.qq.com/s/iaHyYMjZSPBggLw2yZv8dQ) | [JueJin](https://juejin.cn/spost/7266455050632921107) - [Flutter - Quickly achieve half-view exposure statistic 📊](https://medium.com/@linxunfeng/flutter-quickly-achieve-half-view-exposure-statistic-097fd4b237ef) | [WeChat](https://mp.weixin.qq.com/s/gNFX4Au4esftgTPXHvB4LQ) | [JueJin](https://juejin.cn/post/7271248528998121512) - [Flutter - How to quickly implement an contact list page (azlist) 📓](https://medium.com/@linxunfeng/flutter-how-to-quickly-implement-an-contact-list-page-azlist-829bbef12d8f) | [WeChat](https://mp.weixin.qq.com/s/1bmYSvtOYX83DLncvnBjqA) | [JueJin](https://juejin.cn/post/7294884963631497254) - [Flutter - Supports observing NestedScrollView, with greater compatibility 😈](https://medium.com/@linxunfeng/flutter-supports-observing-nestedscrollview-with-greater-compatibility-86f49dce7955) | [WeChat](https://mp.weixin.qq.com/s/1dsmRg8q2VJ6HzasLgoVpA) | [JueJin](https://juejin.cn/post/7388444606456840211) - [Flutter - Offset effect in PageView](https://medium.com/@linxunfeng/flutter-offset-effect-in-pageview-cec39572d56a) | [WeChat](https://mp.weixin.qq.com/s/Q8zk89bgr_8bgWQ4F86VUQ) | [JueJin](https://juejin.cn/post/7411516362916216859) - [Flutter - Parallax effect in PageView](https://medium.com/@linxunfeng/flutter-parallax-effect-in-pageview-ccff6b0b34aa) | [WeChat](https://mp.weixin.qq.com/s/Fi-X2eJRWj17sqCcVqbPRQ) | [JueJin](https://juejin.cn/post/7416655730214699017) - [Flutter - Syncing Detail Page TabBar with Modules? Nailed It!](https://medium.com/@linxunfeng/flutter-syncing-detail-page-tabbar-with-modules-nailed-it-60f2ca67e100) | [WeChat](https://mp.weixin.qq.com/s/uLRLzxS4IqmCq0gewTS8kQ) | [JueJin](https://juejin.cn/post/7538868042961911817) - [Flutter - Initial Anchor And Optimization For Detail Pages](https://medium.com/@linxunfeng/flutter-initial-anchor-and-optimization-for-detail-pages-86aa5b9c4186) | [WeChat](https://mp.weixin.qq.com/s/hRPc_eHjl0OSKFKfmj6q_g) | [JueJin](https://juejin.cn/post/7541801054188109850) ## 🔨 Feature > You do not need to change the view you are currently using, just wrap a `ViewObserver` around the view to achieve the following features. - [x] Observing child widgets those are being displayed in ScrollView - [x] Support for scrolling to a specific item in ScrollView - [x] Quickly implement the chat session page effect - [x] Support for keeping IM message position when inserting or updating messages, avoiding jitter. ## 🎀 Support - [x] `PageView` - [x] `ListView` - [x] `SliverList` - [x] `GridView` - [x] `SliverGrid` - [x] Mixing usage of `SliverPersistentHeader`, `SliverList` and `SliverGrid` - [x] `NestedScrollView` - [x] `ScrollView` built by third-party package. ## 🕹 Preview - 🖥 [Online Preview](https://fluttercandies.github.io/flutter_scrollview_observer/) - 🏞 [Sample images](https://github.com/fluttercandies/flutter_scrollview_observer/wiki/Example) ## 📦 Installing Add `scrollview_observer` to your pubspec.yaml file: ```yaml dependencies: scrollview_observer: latest_version ``` Import `scrollview_observer` in files that it will be used: ```dart import 'package:scrollview_observer/scrollview_observer.dart'; ``` ## 📚 Wiki - [Wiki Home](https://github.com/fluttercandies/flutter_scrollview_observer/wiki) - [1、Observing child widgets those are being displayed in the ScrollView](https://github.com/fluttercandies/flutter_scrollview_observer/wiki/1%E3%80%81Observing-child-widgets-those-are-being-displayed-in-the-ScrollView) - [2、Scrolling to the specified index location](https://github.com/fluttercandies/flutter_scrollview_observer/wiki/2%E3%80%81Scrolling-to-the-specified-index-location) - [3、Chat Observer](https://github.com/fluttercandies/flutter_scrollview_observer/wiki/3%E3%80%81Chat-Observer) ## 🖨 About Me - GitHub: [https://github.com/LinXunFeng](https://github.com/LinXunFeng) - Email: [linxunfeng@yeah.net](mailto:linxunfeng@yeah.net) - Blogs: - 全栈行动: [https://fullstackaction.com](https://fullstackaction.com) - 掘金: [https://juejin.cn/user/1820446984512392](https://juejin.cn/user/1820446984512392) ================================================ FILE: analysis_options.yaml ================================================ include: package:flutter_lints/flutter.yaml # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: example/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Web related # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: example/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled. version: revision: bcea432bce54a83306b3c00a7ad0ed98f777348d channel: beta project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: bcea432bce54a83306b3c00a7ad0ed98f777348d base_revision: bcea432bce54a83306b3c00a7ad0ed98f777348d - platform: macos create_revision: bcea432bce54a83306b3c00a7ad0ed98f777348d base_revision: bcea432bce54a83306b3c00a7ad0ed98f777348d # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: example/README.md ================================================ # example A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: example/analysis_options.yaml ================================================ # This file configures the analyzer, which statically analyzes Dart code to # check for errors, warnings, and lints. # # The issues identified by the analyzer are surfaced in the UI of Dart-enabled # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be # invoked from the command line by running `flutter analyze`. # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` # included above or to enable additional rules. A list of all available lints # and their documentation is published at # https://dart-lang.github.io/linter/lints/index.html. # # Instead of disabling a lint rule for the entire project in the # section below, it can also be suppressed for a single line of code # or a specific dart file by using the `// ignore: name_of_lint` and # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options ================================================ FILE: example/android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks ================================================ FILE: example/android/app/build.gradle ================================================ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion flutter.compileSdkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" namespace("com.example.example") minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } ================================================ FILE: example/android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: example/android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: example/android/app/src/main/kotlin/com/example/example/MainActivity.kt ================================================ package com.example.example import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() { } ================================================ FILE: example/android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: example/android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: example/android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: example/android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: example/android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: example/android/build.gradle ================================================ buildscript { ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:8.5.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: example/android/gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Jan 27 23:32:07 CST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: example/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMEDz ================================================ FILE: example/android/settings.gradle ================================================ include ':app' def localPropertiesFile = new File(rootProject.projectDir, "local.properties") def properties = new Properties() assert localPropertiesFile.exists() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" ================================================ FILE: example/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: example/ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 12.0 ================================================ FILE: example/ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: example/ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: example/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: example/ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. ================================================ FILE: example/ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: example/ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: example/ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Example CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName example CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents ================================================ FILE: example/ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: example/ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; E11A61691B6E95337D3F0498 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B26494B2AD201ECD0581E92 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 1B26494B2AD201ECD0581E92 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A2B511463E682CF908AFE6AD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; B302A33B7464FAB28FCF59DF /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; DCE1D9B7BDC141B768B2DFF1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( E11A61691B6E95337D3F0498 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 3C2B79DE555213845C9D090F /* Pods */ = { isa = PBXGroup; children = ( DCE1D9B7BDC141B768B2DFF1 /* Pods-Runner.debug.xcconfig */, A2B511463E682CF908AFE6AD /* Pods-Runner.release.xcconfig */, B302A33B7464FAB28FCF59DF /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; 741C0A3D2C9A06ADB0AB4AB9 /* Frameworks */ = { isa = PBXGroup; children = ( 1B26494B2AD201ECD0581E92 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 3C2B79DE555213845C9D090F /* Pods */, 741C0A3D2C9A06ADB0AB4AB9 /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 2620F42EECCB6FD7139BCDA0 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 4F73B9F7F81B552833BFB55E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 2620F42EECCB6FD7139BCDA0 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 4F73B9F7F81B552833BFB55E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 2RA5LSHPUJ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.lxf.scrollviewobserver; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 2RA5LSHPUJ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.lxf.scrollviewobserver; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 2RA5LSHPUJ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.lxf.scrollviewobserver; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: example/ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: example/lib/common/route/navigation_service.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 15:49:58 */ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class NavigationService { static final GlobalKey navigatorKey = GlobalKey(); static BuildContext get context => navigatorKey.currentState!.context; static void push( String location, { Object? extra, }) { context.push( location, extra: extra, ); } static void pop() => context.pop(); static dynamic getParam( String key, { dynamic defaultValue, }) { final state = routerState(); final pathParam = state.pathParameters[key]; if (pathParam != null) return pathParam; final extra = stateExtra(); if (extra == null) { return defaultValue; } if (extra is Map) { return extra[key] ?? defaultValue; } return defaultValue; } static GoRouterState routerState() { return GoRouter.of(context).state; } static Object? stateExtra() { return routerState().extra; } } ================================================ FILE: example/lib/common/route/route.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 15:48:51 */ export 'navigation_service.dart'; export 'router_config.dart'; ================================================ FILE: example/lib/common/route/router_config.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 15:49:38 */ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:scrollview_observer_example/common/route/route.dart'; import 'package:scrollview_observer_example/features/custom_scrollview/custom_scrollview_demo/custom_scrollview_center_demo_page.dart'; import 'package:scrollview_observer_example/features/custom_scrollview/custom_scrollview_demo/custom_scrollview_demo_page.dart'; import 'package:scrollview_observer_example/features/custom_scrollview/custom_scrollview_demo/multi_sliver_demo_page.dart'; import 'package:scrollview_observer_example/features/custom_scrollview/sliver_appbar_demo/sliver_appbar_demo_page.dart'; import 'package:scrollview_observer_example/features/gridview/gridview_ctx_demo/gridview_ctx_demo_page.dart'; import 'package:scrollview_observer_example/features/gridview/gridview_custom_demo/gridview_custom_demo_page.dart'; import 'package:scrollview_observer_example/features/gridview/gridview_demo/gridview_demo_page.dart'; import 'package:scrollview_observer_example/features/gridview/gridview_fixed_height_demo/gridview_fixed_height_demo_page.dart'; import 'package:scrollview_observer_example/features/gridview/horizontal_gridview_demo/horizontal_gridview_demo_page.dart'; import 'package:scrollview_observer_example/features/gridview/sliver_grid_demo/sliver_grid_demo_page.dart'; import 'package:scrollview_observer_example/features/home/home_page.dart'; import 'package:scrollview_observer_example/features/listview/horizontal_listview_demo/horizontal_listview_page.dart'; import 'package:scrollview_observer_example/features/listview/infinite_listview_demo/infinite_listview_page.dart'; import 'package:scrollview_observer_example/features/listview/listview_ctx_demo/listview_ctx_demo_page.dart'; import 'package:scrollview_observer_example/features/listview/listview_custom_demo/listview_custom_demo_page.dart'; import 'package:scrollview_observer_example/features/listview/listview_demo/listview_demo_page.dart'; import 'package:scrollview_observer_example/features/listview/listview_dynamic_offset/listview_dynamic_offset_page.dart'; import 'package:scrollview_observer_example/features/listview/listview_fixed_height_demo/listview_fixed_height_demo_page.dart'; import 'package:scrollview_observer_example/features/listview/sliver_list_demo/sliver_list_demo_page.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_demo/nested_scrollview_demo_page.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/page/nested_scrollview_tab_bar_view_demo_page.dart'; import 'package:scrollview_observer_example/features/pageview/pageview_demo/pageview_demo_page.dart'; import 'package:scrollview_observer_example/features/pageview/pageview_demo/pageview_parallax_item_listener_page.dart'; import 'package:scrollview_observer_example/features/pageview/pageview_demo/pageview_parallax_page.dart'; import 'package:scrollview_observer_example/features/scene/anchor_demo/anchor_page.dart'; import 'package:scrollview_observer_example/features/scene/anchor_demo/anchor_waterfall_page.dart'; import 'package:scrollview_observer_example/features/scene/azlist_demo/azlist_page.dart'; import 'package:scrollview_observer_example/features/scene/chat_demo/page/chat_gpt_page.dart'; import 'package:scrollview_observer_example/features/scene/chat_demo/page/chat_page.dart'; import 'package:scrollview_observer_example/features/scene/detail/page/detail_page.dart'; import 'package:scrollview_observer_example/features/scene/expandable_carousel_slider_demo/expandable_carousel_slider_demo.dart'; import 'package:scrollview_observer_example/features/scene/image_tab_demo/image_tab_page.dart'; import 'package:scrollview_observer_example/features/scene/scrollview_form_demo/scrollview_form_demo_page.dart'; import 'package:scrollview_observer_example/features/scene/video_auto_play_list/video_list_auto_play_page.dart'; import 'package:scrollview_observer_example/features/scene/visibility_demo/page/visibility_listview_page.dart'; import 'package:scrollview_observer_example/features/scene/visibility_demo/page/visibility_scrollview_page.dart'; import 'package:scrollview_observer_example/features/scene/waterfall_flow_demo/waterfall_flow_page.dart'; import 'package:scrollview_observer_example/features/scene/waterfall_flow_fixed_height_demo/waterfall_flow_fixed_height_page.dart'; class MyPage { static const home = '/home'; // ListView static const listView = '/list_view'; static const listViewContext = '/list_view_context'; static const listViewFixedHeight = '/list_view_fixed_height'; static const listViewHorizontal = '/list_view_horizontal'; static const listViewDynamicOffset = '/list_view_dynamic_offset'; static const listViewCustom = '/list_view_custom'; static const listViewInfinite = '/list_view_infinite'; static const sliverListView = '/sliver_list_view'; // GridView static const gridView = '/grid_view'; static const gridViewContext = '/grid_view_context'; static const gridViewFixedHeight = '/grid_view_fixed_height'; static const gridViewHorizontal = '/grid_view_horizontal'; static const gridViewCustom = '/grid_view_custom'; static const sliverGridView = '/sliver_grid_view'; // CustomScrollView static const customScrollView = '/custom_scroll_view'; static const customScrollViewCenter = '/custom_scroll_view_center'; static const multiSliver = '/multi_sliver'; static const sliverAppBar = '/sliver_app_bar'; // NestedScrollView static const nestedScrollView = '/nested_scroll_view'; static const nestedScrollViewTabBarView = '/nested_scroll_view_tab_bar_view'; // PageView static const pageView = '/page_view'; static const pageViewParallax = '/page_view_parallax'; static const pageViewParallaxItemListener = '/page_view_parallax_item_listener'; // Scene static const videoListAutoPlay = '/video_list_auto_play'; static const anchorList = '/anchor_list'; static const anchorWaterfall = '/anchor_waterfall'; static const imageTab = '/image_tab'; static const chat = '/chat'; static const chatGPT = '/chat_gpt'; static const waterfallFlow = '/waterfall_flow'; static const waterfallFlowFixedHeight = '/waterfall_flow_fixed_height'; static const scrollViewForm = '/scroll_view_form'; static const visibilityListView = '/visibility_list_view'; static const visibilityScrollView = '/visibility_scroll_view'; static const azList = '/az_list'; static const expandableCarouselSlider = '/expandable_carousel_slider'; static const detail = '/detail'; } class MyRoute { static final observer = RouteObserver(); static final routerConfig = GoRouter( routes: routes, initialLocation: MyPage.home, observers: [ observer, ], navigatorKey: NavigationService.navigatorKey, debugLogDiagnostics: !kReleaseMode, ); static final List routes = [ GoRoute( path: MyPage.home, builder: (context, state) => const HomePage(), ), GoRoute( path: MyPage.listView, builder: (context, state) => const ListViewDemoPage(), ), GoRoute( path: MyPage.listViewContext, builder: (context, state) => const ListViewCtxDemoPage(), ), GoRoute( path: MyPage.listViewFixedHeight, builder: (context, state) => const ListViewFixedHeightDemoPage(), ), GoRoute( path: MyPage.listViewHorizontal, builder: (context, state) => const HorizontalListViewPage(), ), GoRoute( path: MyPage.listViewDynamicOffset, builder: (context, state) => const ListViewDynamicOffsetPage(), ), GoRoute( path: MyPage.listViewCustom, builder: (context, state) => const ListViewCustomDemoPage(), ), GoRoute( path: MyPage.listViewInfinite, builder: (context, state) => const InfiniteListViewPage(), ), GoRoute( path: MyPage.sliverListView, builder: (context, state) => const SliverListViewDemoPage(), ), GoRoute( path: MyPage.gridView, builder: (context, state) => const GridViewDemoPage(), ), GoRoute( path: MyPage.gridViewContext, builder: (context, state) => const GridViewCtxDemoPage(), ), GoRoute( path: MyPage.gridViewFixedHeight, builder: (context, state) => const GridViewFixedHeightDemoPage(), ), GoRoute( path: MyPage.gridViewHorizontal, builder: (context, state) => const HorizontalGridViewDemoPage(), ), GoRoute( path: MyPage.gridViewCustom, builder: (context, state) => const GridViewCustomDemoPage(), ), GoRoute( path: MyPage.sliverGridView, builder: (context, state) => const SliverGridViewDemoPage(), ), GoRoute( path: MyPage.customScrollView, builder: (context, state) => const CustomScrollViewDemoPage(), ), GoRoute( path: MyPage.customScrollViewCenter, builder: (context, state) => const CustomScrollViewCenterDemoPage(), ), GoRoute( path: MyPage.multiSliver, builder: (context, state) => const MultiSliverDemoPage(), ), GoRoute( path: MyPage.sliverAppBar, builder: (context, state) => const SliverAppBarDemoPage(), ), GoRoute( path: MyPage.nestedScrollView, builder: (context, state) => const NestedScrollViewDemoPage(), ), GoRoute( path: MyPage.nestedScrollViewTabBarView, builder: (context, state) => const NestedScrollViewTabBarViewDemoPage(), ), GoRoute( path: MyPage.pageView, builder: (context, state) => const PageViewDemoPage(), ), GoRoute( path: MyPage.pageViewParallax, builder: (context, state) => const PageViewParallaxPage(), ), GoRoute( path: MyPage.pageViewParallaxItemListener, builder: (context, state) => const PageViewParallaxItemListenerPage(), ), GoRoute( path: MyPage.videoListAutoPlay, builder: (context, state) => const VideoListAutoPlayPage(), ), GoRoute( path: MyPage.anchorList, builder: (context, state) => const AnchorListPage(), ), GoRoute( path: MyPage.anchorWaterfall, builder: (context, state) => const AnchorWaterfallPage(), ), GoRoute( path: MyPage.imageTab, builder: (context, state) => const ImageTabPage(), ), GoRoute( path: MyPage.chat, builder: (context, state) => const ChatPage(), ), GoRoute( path: MyPage.chatGPT, builder: (context, state) => const ChatGPTPage(), ), GoRoute( path: MyPage.waterfallFlow, builder: (context, state) => const WaterfallFlowPage(), ), GoRoute( path: MyPage.waterfallFlowFixedHeight, builder: (context, state) => const WaterfallFlowFixedHeightPage(), ), GoRoute( path: MyPage.scrollViewForm, builder: (context, state) => const ScrollViewFormDemoPage(), ), GoRoute( path: MyPage.visibilityListView, builder: (context, state) => const VisibilityListViewPage(), ), GoRoute( path: MyPage.visibilityScrollView, builder: (context, state) => const VisibilityScrollViewPage(), ), GoRoute( path: MyPage.azList, builder: (context, state) => const AzListPage(), ), GoRoute( path: MyPage.expandableCarouselSlider, builder: (context, state) => const ExpandableCarouselSliderDemo(), ), GoRoute( path: MyPage.detail, builder: (context, state) => const DetailPage(), ), ]; } ================================================ FILE: example/lib/features/custom_scrollview/custom_scrollview_demo/custom_scrollview_center_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-03-11 21:08:23 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; class CustomScrollViewCenterDemoPage extends StatefulWidget { const CustomScrollViewCenterDemoPage({Key? key}) : super(key: key); @override State createState() => _CustomScrollViewCenterDemoPageState(); } class _CustomScrollViewCenterDemoPageState extends State { BuildContext? _sliverListCtx1; BuildContext? _sliverListCtx2; BuildContext? _sliverListCtx3; BuildContext? _sliverListCtx4; final _centerKey = GlobalKey(); ScrollController scrollController = ScrollController(); late SliverObserverController observerController; @override void initState() { super.initState(); observerController = SliverObserverController(controller: scrollController) ..cacheJumpIndexOffset = false; } @override void dispose() { observerController.controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("CustomScrollView - center")), body: SliverViewObserver( controller: observerController, child: _buildScrollView(), sliverContexts: () { return [ if (_sliverListCtx1 != null) _sliverListCtx1!, if (_sliverListCtx2 != null) _sliverListCtx2!, if (_sliverListCtx3 != null) _sliverListCtx3!, if (_sliverListCtx4 != null) _sliverListCtx4!, ]; }, onObserveAll: (resultMap) { final model1 = resultMap[_sliverListCtx1]; if (model1 != null && model1.visible && model1 is ListViewObserveModel) { debugPrint('1 visible -- ${model1.visible}'); debugPrint('1 firstChild.index -- ${model1.firstChild?.index}'); debugPrint('1 displaying -- ${model1.displayingChildIndexList}'); } final model2 = resultMap[_sliverListCtx2]; if (model2 != null && model2.visible && model2 is ListViewObserveModel) { debugPrint('2 visible -- ${model2.visible}'); debugPrint('2 firstChild.index -- ${model2.firstChild?.index}'); debugPrint('2 displaying -- ${model2.displayingChildIndexList}'); } final model3 = resultMap[_sliverListCtx3]; if (model3 != null && model3.visible && model3 is ListViewObserveModel) { debugPrint('3 visible -- ${model3.visible}'); debugPrint('3 firstChild.index -- ${model3.firstChild?.index}'); debugPrint('3 displaying -- ${model3.displayingChildIndexList}'); } final model4 = resultMap[_sliverListCtx4]; if (model4 != null && model4.visible && model4 is ListViewObserveModel) { debugPrint('4 visible -- ${model4.visible}'); debugPrint('4 firstChild.index -- ${model4.firstChild?.index}'); debugPrint('4 displaying -- ${model4.displayingChildIndexList}'); } }, ), floatingActionButton: Padding( padding: const EdgeInsets.all(15.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'SliverList - Jumping to row 29', ); observerController.animateTo( sliverContext: _sliverListCtx1, index: 29, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); }, icon: const Icon(Icons.ac_unit_outlined), ), IconButton( onPressed: () { SnackBarUtil.showSnackBar( context: context, text: '_sliverListCtx2 - Jumping to item 20', ); observerController.animateTo( sliverContext: _sliverListCtx2, index: 20, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); }, icon: const Icon(Icons.backup_table), ), IconButton( onPressed: () { SnackBarUtil.showSnackBar( context: context, text: '_sliverListCtx3 - Jumping to item 20', ); observerController.animateTo( sliverContext: _sliverListCtx3, index: 20, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, alignment: 1, ); }, icon: const Icon(Icons.cabin), ), IconButton( onPressed: () { SnackBarUtil.showSnackBar( context: context, text: '_sliverListCtx4 - Jumping to item 20', ); observerController.animateTo( sliverContext: _sliverListCtx4, index: 20, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, alignment: 1, ); }, icon: const Icon(Icons.cruelty_free), ), ], ), ), ); } Widget _buildScrollView() { return CustomScrollView( center: _centerKey, anchor: 1, controller: scrollController, slivers: [ _buildSliverListView( color: Colors.redAccent, onBuild: (ctx) { _sliverListCtx1 = ctx; }, ), _buildSliverListView( color: Colors.blueGrey, onBuild: (ctx) { _sliverListCtx2 = ctx; }, ), SliverPadding(padding: EdgeInsets.zero, key: _centerKey), _buildSliverListView( color: Colors.teal, onBuild: (ctx) { _sliverListCtx3 = ctx; }, ), _buildSliverListView( color: Colors.purple, onBuild: (ctx) { _sliverListCtx4 = ctx; }, ), ], ); } Widget _buildSliverListView({ required Color color, Function(BuildContext)? onBuild, }) { return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { onBuild?.call(ctx); final int itemIndex = index ~/ 2; return Container( height: (itemIndex % 2 == 0) ? 80 : 50, color: color, child: Center( child: Text( "index -- $index", style: const TextStyle(color: Colors.white), ), ), ); }, childCount: 100, ), ); } } ================================================ FILE: example/lib/features/custom_scrollview/custom_scrollview_demo/custom_scrollview_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; class CustomScrollViewDemoPage extends StatefulWidget { const CustomScrollViewDemoPage({Key? key}) : super(key: key); @override State createState() => _CustomScrollViewDemoPageState(); } class _CustomScrollViewDemoPageState extends State { BuildContext? _sliverListCtx; BuildContext? _sliverGridCtx; int _hitIndexForCtx1 = 0; List _hitIndexsForGrid = []; 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!, ); }); } @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: _buildScrollView(), sliverContexts: () { return [ if (_sliverListCtx != null) _sliverListCtx!, if (_sliverGridCtx != null) _sliverGridCtx!, ]; }, onObserveAll: (resultMap) { final model1 = resultMap[_sliverListCtx]; if (model1 != null && model1.visible && model1 is ListViewObserveModel) { debugPrint('1 visible -- ${model1.visible}'); debugPrint('1 firstChild.index -- ${model1.firstChild?.index}'); debugPrint('1 displaying -- ${model1.displayingChildIndexList}'); setState(() { _hitIndexForCtx1 = model1.firstChild?.index ?? 0; }); } final model2 = resultMap[_sliverGridCtx]; if (model2 != null && model2.visible && model2 is GridViewObserveModel) { debugPrint('2 visible -- ${model2.visible}'); debugPrint('2 displaying -- ${model2.displayingChildIndexList}'); setState(() { _hitIndexsForGrid = model2.firstGroupChildList.map((e) => e.index).toList(); }); } }, ), floatingActionButton: Padding( padding: const EdgeInsets.all(15.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'SliverList - Jumping to row 29', ); observerController.animateTo( sliverContext: _sliverListCtx, index: 29, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); }, icon: const Icon(Icons.ac_unit_outlined), ), IconButton( onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'SliverGrid - Jumping to item 20', ); observerController.animateTo( sliverContext: _sliverGridCtx, index: 20, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); }, icon: const Icon(Icons.backup_table), ), ], ), ), ); } Widget _buildScrollView() { return CustomScrollView( controller: scrollController, // scrollDirection: Axis.horizontal, slivers: [ _buildSliverListView(), _buildSliverGridView(), ], ); } Widget _buildSliverListView() { // return SliverFixedExtentList( // delegate: SliverChildBuilderDelegate( // (ctx, index) { // _sliverListCtx ??= ctx; // return Container( // // height: (index % 2 == 0) ? 80 : 50, // color: _hitIndexForCtx1 == index ? Colors.red : Colors.black12, // child: Center( // child: Text( // "index -- $index", // style: TextStyle( // color: // _hitIndexForCtx1 == index ? Colors.white : Colors.black, // ), // ), // ), // ); // }, // childCount: 30, // ), // itemExtent: 100, // ); return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { _sliverListCtx ??= ctx; return Container( height: (index % 2 == 0) ? 80 : 50, color: _hitIndexForCtx1 == index ? Colors.red : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: _hitIndexForCtx1 == index ? Colors.white : Colors.black, ), ), ), ); }, childCount: 30, ), ); } Widget _buildSliverGridView() { return SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, //Grid按两列显示 mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 2.0, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { _sliverGridCtx ??= context; return Container( color: (_hitIndexsForGrid.contains(index)) ? Colors.green : Colors.blue[100], child: Center( child: Text('index -- $index'), ), ); }, childCount: 150, ), ); } } ================================================ FILE: example/lib/features/custom_scrollview/custom_scrollview_demo/multi_sliver_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-09-16 19:41:33 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/utils/random.dart'; class MultiSliverDemoModel { final String tag; final List value; const MultiSliverDemoModel({ required this.tag, required this.value, }); } class MultiSliverDemoPage extends StatefulWidget { const MultiSliverDemoPage({Key? key}) : super(key: key); @override State createState() => _MultiSliverDemoPageState(); } class _MultiSliverDemoPageState extends State { List modelList = []; final appBarKey = GlobalKey(); final scrollController = ScrollController(); late final SliverObserverController sliverItemObserverController; // late final SliverObserverController sliverObserverController; Map itemSliverIndexCtxMap = {}; Map sliverIndexCtxMap = {}; ValueNotifier tabCurrentSelectedIndex = ValueNotifier(0); bool isIgnoreCalcTabBarIndex = false; @override void initState() { super.initState(); sliverItemObserverController = SliverObserverController( controller: scrollController, ); // sliverObserverController = SliverObserverController( // controller: scrollController, // ); for (var i = 0; i < 4; i++) { final tag = 'Section ${i + 1}'; List values = []; for (var j = 0; j < 4; j++) { values.add('Row: ${i + 1}-$j'); } modelList.add(MultiSliverDemoModel(tag: tag, value: values)); } } @override Widget build(BuildContext context) { Widget resultWidget = _buildScrollView(); resultWidget = _buildSliverItemObserver(child: resultWidget); resultWidget = _buildSliverObserver(child: resultWidget); return Scaffold( body: resultWidget, bottomNavigationBar: buildBottomNavigationBar(context), ); } /// To observe sliver items and handle scrollTo. Widget _buildSliverItemObserver({ required Widget child, }) { return SliverViewObserver( controller: sliverItemObserverController, sliverContexts: () => itemSliverIndexCtxMap.values.toList(), child: child, ); } /// To observe which sliver is currently the first. Widget _buildSliverObserver({ required Widget child, }) { return SliverViewObserver( // controller: sliverObserverController, child: child, sliverContexts: () => sliverIndexCtxMap.values.toList(), triggerOnObserveType: ObserverTriggerOnObserveType.directly, dynamicLeadingOffset: () { // Accumulate the height of all PersistentHeader. return ObserverUtils.calcPersistentHeaderExtent( key: appBarKey, offset: scrollController.offset, ) + 1; // To avoid tabBar index rebound. }, onObserveViewport: (result) { if (isIgnoreCalcTabBarIndex) return; int? currentTabIndex; final currentFirstSliverCtx = result.firstChild.sliverContext; for (var sectionIndex in sliverIndexCtxMap.keys) { final ctx = sliverIndexCtxMap[sectionIndex]; if (ctx == null) continue; // If they are not the same sliver, continue. if (currentFirstSliverCtx != ctx) continue; // If the sliver is not visible, continue. final visible = (ctx.findRenderObject() as RenderSliver).geometry?.visible ?? false; if (!visible) continue; currentTabIndex = sectionIndex; break; } if (currentTabIndex == null) return; updateTabBarIndex(currentTabIndex); }, ); } Widget _buildScrollView() { return CustomScrollView( controller: scrollController, physics: const ClampingScrollPhysics(), slivers: [ SliverAppBar( key: appBarKey, pinned: true, title: const Text('Multi Sliver'), ), ...List.generate(modelList.length, (mainIndex) { return _buildSectionListView(mainIndex); }), ], ); } Widget buildBottomNavigationBar(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: List.generate(modelList.length, (index) { return Expanded( child: InkWell( onTap: () async { updateTabBarIndex(index); isIgnoreCalcTabBarIndex = true; // await sliverItemObserverController.jumpTo( // sliverContext: itemSliverIndexCtxMap[index], // index: 0, // isFixedHeight: true, // offset: (offset) { // return ObserverUtils.calcPersistentHeaderExtent( // key: appBarKey, // offset: offset, // ); // }, // ); await sliverItemObserverController.animateTo( sliverContext: itemSliverIndexCtxMap[index], index: 0, isFixedHeight: true, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, offset: (offset) { return ObserverUtils.calcPersistentHeaderExtent( key: appBarKey, offset: offset, ); }, ); isIgnoreCalcTabBarIndex = false; }, child: ValueListenableBuilder( valueListenable: tabCurrentSelectedIndex, builder: (BuildContext context, int value, Widget? child) { return Container( alignment: Alignment.center, height: 40, decoration: BoxDecoration( border: Border.all(width: 0.5), color: value == index ? Colors.amber : Colors.white, ), child: Text( modelList[index].tag, ), ); }, ), ), ); }), ), SizedBox(height: MediaQuery.paddingOf(context).bottom), ], ); } Widget _buildSectionListView(int mainIndex) { Widget resultWidget = SliverStickyHeader( header: Container( height: 40, color: Colors.white, padding: const EdgeInsets.only(left: 12), alignment: Alignment.centerLeft, child: Text( modelList[mainIndex].tag, ), ), sliver: SliverFixedExtentList( itemExtent: 120, delegate: SliverChildBuilderDelegate( (context, index) { // Save the context of SliverList. itemSliverIndexCtxMap[mainIndex] = context; return Container( padding: const EdgeInsets.only(left: 12), color: RandomTool.color(), alignment: Alignment.centerLeft, child: Text( modelList[mainIndex].value[index], ), ); }, childCount: modelList[mainIndex].value.length, ), ), ); resultWidget = SliverObserveContext( child: resultWidget, onObserve: (context) { // Save the context of the outermost sliver. sliverIndexCtxMap[mainIndex] = context; }, ); return resultWidget; } updateTabBarIndex(int index) { if (index == tabCurrentSelectedIndex.value) return; tabCurrentSelectedIndex.value = index; } } ================================================ FILE: example/lib/features/custom_scrollview/sliver_appbar_demo/sliver_appbar_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-20 09:22:52 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; class SliverAppBarDemoPage extends StatefulWidget { const SliverAppBarDemoPage({Key? key}) : super(key: key); @override State createState() => _SliverAppBarDemoPageState(); } class _SliverAppBarDemoPageState extends State { BuildContext? _sliverListCtx; BuildContext? _sliverGridCtx; GlobalKey appBarKey = GlobalKey(); int _hitIndexForCtx1 = 0; List _hitIndexsForGrid = []; ScrollController scrollController = ScrollController(); late SliverObserverController observerController; @override void initState() { super.initState(); observerController = SliverObserverController(controller: scrollController) ..initialIndexModelBlock = () { return ObserverIndexPositionModel( index: 6, sliverContext: _sliverListCtx, offset: calcPersistentHeaderExtent, ); }; // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { observerController.dispatchOnceObserve( sliverContext: _sliverListCtx!, ); }); } @override void dispose() { observerController.controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: SliverViewObserver( controller: observerController, child: _buildScrollView(), sliverContexts: () { return [ if (_sliverListCtx != null) _sliverListCtx!, if (_sliverGridCtx != null) _sliverGridCtx!, ]; }, autoTriggerObserveTypes: const [ ObserverAutoTriggerObserveType.scrollEnd, ], triggerOnObserveType: ObserverTriggerOnObserveType.directly, onObserveAll: (resultMap) { final model1 = resultMap[_sliverListCtx]; if (model1 != null && model1.visible && model1 is ListViewObserveModel) { debugPrint('1 visible -- ${model1.visible}'); debugPrint('1 firstChild.index -- ${model1.firstChild?.index}'); debugPrint('1 firstChild.size -- ${model1.firstChild?.size}'); debugPrint('1 displaying -- ${model1.displayingChildIndexList}'); debugPrint( '1 displaying -- index${model1.firstChild?.index} -- ${model1.firstChild?.displayPercentage}'); setState(() { _hitIndexForCtx1 = model1.firstChild?.index ?? 0; }); } final model2 = resultMap[_sliverGridCtx]; if (model2 != null && model2.visible && model2 is GridViewObserveModel) { debugPrint('2 visible -- ${model2.visible}'); debugPrint('2 displaying -- ${model2.displayingChildIndexList}'); setState(() { _hitIndexsForGrid = model2.firstGroupChildList.map((e) => e.index).toList(); }); } }, ), floatingActionButton: Padding( padding: const EdgeInsets.all(15.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'SliverList - Jumping to row 8', ); observerController.animateTo( sliverContext: _sliverListCtx, index: 8, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, offset: calcPersistentHeaderExtent, ); }, icon: const Icon(Icons.ac_unit_outlined), ), IconButton( onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'SliverGrid - Jumping to item 5', ); observerController.animateTo( sliverContext: _sliverGridCtx, index: 5, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, offset: calcPersistentHeaderExtent, ); }, icon: const Icon(Icons.backup_table), ), ], ), ), ); } Widget _buildScrollView() { return CustomScrollView( controller: scrollController, // scrollDirection: Axis.horizontal, slivers: [ _buildSliverAppBar(), _buildSliverListView(), _buildSliverGridView(), ], cacheExtent: double.maxFinite, ); } Widget _buildSliverAppBar() { return SliverAppBar( key: appBarKey, pinned: true, expandedHeight: 200, flexibleSpace: FlexibleSpaceBar( title: const Text('AppBar'), background: Container(color: Colors.orange), ), ); } Widget _buildSliverListView() { return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { _sliverListCtx ??= ctx; return Container( height: (index % 2 == 0) ? 80 : 50, color: _hitIndexForCtx1 == index ? Colors.red : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: _hitIndexForCtx1 == index ? Colors.white : Colors.black, ), ), ), ); }, childCount: 20, ), ); } Widget _buildSliverGridView() { return SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 2.0, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { _sliverGridCtx ??= context; return Container( color: (_hitIndexsForGrid.contains(index)) ? Colors.green : Colors.blue[100], child: Center( child: Text('index -- $index'), ), ); }, childCount: 20, ), ); } double calcPersistentHeaderExtent(double offset) { return ObserverUtils.calcPersistentHeaderExtent( key: appBarKey, offset: offset, ); } } ================================================ FILE: example/lib/features/gridview/gridview_ctx_demo/gridview_ctx_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; class GridViewCtxDemoPage extends StatefulWidget { const GridViewCtxDemoPage({Key? key}) : super(key: key); @override State createState() => _GridViewCtxDemoPageState(); } class _GridViewCtxDemoPageState extends State { BuildContext? _sliverGridViewContext; List _hitIndexs = []; @override void initState() { super.initState(); // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { GridViewOnceObserveNotification().dispatch(_sliverGridViewContext); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("GridView")), body: GridViewObserver( sliverGridContexts: () { return [if (_sliverGridViewContext != null) _sliverGridViewContext!]; }, onObserveAll: (resultMap) { final model = resultMap[_sliverGridViewContext]; if (model == null) return; setState(() { _hitIndexs = model.firstGroupChildList.map((e) => e.index).toList(); }); debugPrint( 'firstGroupChildList -- ${model.firstGroupChildList.map((e) => e.index)}'); debugPrint('displaying -- ${model.displayingChildIndexList}'); }, child: _buildGridView(), ), ); } Widget _buildGridView() { return GridView.builder( padding: const EdgeInsets.only(top: 1000, bottom: 1000), controller: ScrollController(initialScrollOffset: 1000), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 2, mainAxisSpacing: 5, ), // gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( // maxCrossAxisExtent: 140.0, // childAspectRatio: 0.6, // crossAxisSpacing: 2, // mainAxisSpacing: 5, // ), itemBuilder: (context, index) { if (_sliverGridViewContext != context) { _sliverGridViewContext = context; } return Container( color: (_hitIndexs.contains(index)) ? Colors.red : Colors.blue[100], child: Center( child: Text('index -- $index'), ), ); }, itemCount: 50, ); } } ================================================ FILE: example/lib/features/gridview/gridview_custom_demo/gridview_custom_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-05-21 11:14:46 */ // ignore: implementation_imports import 'package:extended_list/src/rendering/sliver_grid.dart'; import 'package:flutter/material.dart'; import 'package:loading_more_list/loading_more_list.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; class GridViewCustomDemoPage extends StatefulWidget { const GridViewCustomDemoPage({Key? key}) : super(key: key); @override State createState() => _GridViewCustomDemoPageState(); } class _GridViewCustomDemoPageState extends State { ScrollController scrollController = ScrollController(); late GridObserverController observerController; @override void initState() { observerController = GridObserverController( controller: scrollController, ); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Custom')), body: GridViewObserver( child: _buildGridView(), controller: observerController, customTargetRenderSliverType: (renderObj) { // Here you tell the package what type of RenderObject it needs to observe. return renderObj is ExtendedRenderSliverGrid; }, // customHandleObserve: (context) { // // Here you can customize the observation logic. // return ObserverCore.handleGridObserve( // context: context, // fetchLeadingOffset: () => 100, // ); // }, onObserve: (resultModel) { debugPrint( 'firstChild.index -- ${resultModel.firstGroupChildList.map((e) => e.index)}'); debugPrint('displaying -- ${resultModel.displayingChildIndexList}'); }, ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.airline_stops_sharp), onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'Jump to item 10', ); observerController.jumpTo( index: 10, ); }, ), ); } Widget _buildGridView() { return LoadingMoreList( ListConfig( controller: scrollController, itemBuilder: (context, item, index) { if (scrollController.hasClients && (observerController.sliverContexts.isEmpty || observerController.sliverContexts.first != context)) { observerController.reattach(); } return Container( color: Colors.cyan, child: ListTile( title: Text('index - $index'), ), ); }, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 300.0, crossAxisSpacing: 3.0, mainAxisSpacing: 3.0, ), sourceList: SourceList(), ), ); } } class SourceList extends LoadingMoreBase { @override Future loadData([bool isloadMoreAction = false]) async { await Future.delayed(const Duration(seconds: 2)); for (var i = 0; i < 30; i++) { add(i); } return true; } } ================================================ FILE: example/lib/features/gridview/gridview_demo/gridview_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; class GridViewDemoPage extends StatefulWidget { const GridViewDemoPage({Key? key}) : super(key: key); @override State createState() => _GridViewDemoPageState(); } class _GridViewDemoPageState extends State { static const double _leadingPadding = 1000; static const double _trailingPadding = 100; static const EdgeInsets _padding = EdgeInsets.only( top: _leadingPadding, bottom: _trailingPadding, ); List _hitIndexs = [0, 1]; ScrollController scrollController = ScrollController(initialScrollOffset: _leadingPadding); late GridObserverController observerController; @override void initState() { super.initState(); observerController = GridObserverController(controller: scrollController); // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.endOfFrame.then( (_) { if (mounted) { // After layout observerController.dispatchOnceObserve(); } }, ); } @override void dispose() { observerController.controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("GridView")), body: GridViewObserver( controller: observerController, onObserve: (result) { final model = result; setState(() { _hitIndexs = model.firstGroupChildList.map((e) => e.index).toList(); }); debugPrint( 'firstGroupChildList -- ${model.firstGroupChildList.map((e) => e.index)}'); debugPrint('displaying -- ${model.displayingChildIndexList}'); }, child: _buildGridView(), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.airline_stops_outlined), onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'Jump to item 49', ); observerController.jumpTo( index: 49, padding: _padding, ); // observerController.animateTo( // index: 49, // duration: const Duration(seconds: 1), // curve: Curves.ease, // ); }, ), ); } Widget _buildGridView() { return GridView.builder( padding: _padding, controller: scrollController, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 2, mainAxisSpacing: 5, ), itemBuilder: (context, index) { return Container( color: (_hitIndexs.contains(index)) ? Colors.red : Colors.blue[100], child: Center( child: Text('index -- $index'), ), ); }, itemCount: 50, ); } } ================================================ FILE: example/lib/features/gridview/gridview_fixed_height_demo/gridview_fixed_height_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; class GridViewFixedHeightDemoPage extends StatefulWidget { const GridViewFixedHeightDemoPage({Key? key}) : super(key: key); @override State createState() => _GridViewFixedHeightDemoPageState(); } class _GridViewFixedHeightDemoPageState extends State { static const double _leadingPadding = 1000; static const double _trailingPadding = 100; static const EdgeInsets _padding = EdgeInsets.only( top: _leadingPadding, bottom: _trailingPadding, left: 30, right: 30, ); List _hitIndexs = [0, 1]; ScrollController scrollController = ScrollController(initialScrollOffset: _leadingPadding); late GridObserverController observerController; @override void initState() { super.initState(); observerController = GridObserverController(controller: scrollController); // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.endOfFrame.then( (_) { if (mounted) { // After layout observerController.dispatchOnceObserve(); } }, ); } @override void dispose() { observerController.controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("GridView")), body: GridViewObserver( controller: observerController, onObserve: (result) { final model = result; setState(() { _hitIndexs = model.firstGroupChildList.map((e) => e.index).toList(); }); debugPrint( 'firstGroupChildList -- ${model.firstGroupChildList.map((e) => e.index)}'); debugPrint('displaying -- ${model.displayingChildIndexList}'); }, child: _buildGridView(), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.airline_stops_outlined), onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'Jump to item 21', ); observerController.jumpTo( index: 21, padding: _padding, isFixedHeight: true, ); }, ), ); } Widget _buildGridView() { return GridView.builder( padding: _padding, controller: scrollController, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 2, mainAxisSpacing: 5, ), itemBuilder: (context, index) { return Container( color: (_hitIndexs.contains(index)) ? Colors.red : Colors.blue[100], child: Center( child: Text('index -- $index'), ), ); }, itemCount: 50, ); } } ================================================ FILE: example/lib/features/gridview/horizontal_gridview_demo/horizontal_gridview_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; class HorizontalGridViewDemoPage extends StatefulWidget { const HorizontalGridViewDemoPage({Key? key}) : super(key: key); @override State createState() => _HorizontalGridViewDemoPageState(); } class _HorizontalGridViewDemoPageState extends State { static const double _leadingPadding = 1000; static const double _trailingPadding = 2000; static const EdgeInsets _padding = EdgeInsets.only( left: _leadingPadding, right: _trailingPadding, ); BuildContext? _sliverGridViewContext; List _hitIndexs = []; ScrollController scrollController = ScrollController(initialScrollOffset: _leadingPadding); late GridObserverController observerController; @override void initState() { super.initState(); observerController = GridObserverController(controller: scrollController) ..initialIndexModel = ObserverIndexPositionModel( index: 98, padding: _padding, ); // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { GridViewOnceObserveNotification().dispatch(_sliverGridViewContext); }); } @override void dispose() { observerController.controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("GridView")), body: GridViewObserver( controller: observerController, sliverGridContexts: () { return [if (_sliverGridViewContext != null) _sliverGridViewContext!]; }, autoTriggerObserveTypes: const [ ObserverAutoTriggerObserveType.scrollEnd, ], triggerOnObserveType: ObserverTriggerOnObserveType.displayingItemsChange, onObserveAll: (resultMap) { final model = resultMap[_sliverGridViewContext]; if (model == null) return; setState(() { _hitIndexs = model.firstGroupChildList.map((e) => e.index).toList(); }); debugPrint( 'firstGroupChildList -- ${model.firstGroupChildList.map((e) => e.index)}'); debugPrint('displaying -- ${model.displayingChildIndexList}'); }, child: _buildGridView(), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.airline_stops_outlined), onPressed: () { if (_sliverGridViewContext != null) { SnackBarUtil.showSnackBar( context: context, text: 'Jump to item 87', ); observerController.jumpTo( index: 87, sliverContext: _sliverGridViewContext, padding: _padding, ); } // observerController.jumpTo( // index: 87, // ); // observerController.animateTo( // index: 49, // duration: const Duration(seconds: 1), // curve: Curves.ease, // ); }, ), ); } Widget _buildGridView() { return GridView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.only( left: _leadingPadding, right: _trailingPadding, ), controller: scrollController, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, crossAxisSpacing: 2, mainAxisSpacing: 5, ), itemBuilder: (context, index) { if (_sliverGridViewContext != context) { _sliverGridViewContext = context; } return Container( color: (_hitIndexs.contains(index)) ? Colors.red : Colors.blue[100], child: Center( child: Text('index -- $index'), ), ); }, itemCount: 100, cacheExtent: double.maxFinite, ); } } ================================================ FILE: example/lib/features/gridview/sliver_grid_demo/sliver_grid_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; class SliverGridViewDemoPage extends StatefulWidget { const SliverGridViewDemoPage({Key? key}) : super(key: key); @override State createState() => _SliverGridViewDemoPageState(); } class _SliverGridViewDemoPageState extends State { BuildContext? _sliverGridViewContext1; BuildContext? _sliverGridViewContext2; List _hitIndexs1 = []; List _hitIndexs2 = []; @override void initState() { super.initState(); // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { GridViewOnceObserveNotification().dispatch(_sliverGridViewContext1); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("GridView")), body: GridViewObserver( sliverGridContexts: () { return [ if (_sliverGridViewContext1 != null) _sliverGridViewContext1!, if (_sliverGridViewContext2 != null) _sliverGridViewContext2! ]; }, onObserveAll: (resultMap) { final model1 = resultMap[_sliverGridViewContext1]; if (model1 != null && model1.visible) { setState(() { _hitIndexs1 = model1.firstGroupChildList.map((e) => e.index).toList(); }); debugPrint( '1 -- firstGroupChildList -- ${model1.firstGroupChildList.map((e) => e.index)}'); debugPrint('1 -- displaying -- ${model1.displayingChildIndexList}'); } final model2 = resultMap[_sliverGridViewContext2]; if (model2 != null && model2.visible) { setState(() { _hitIndexs2 = model2.firstGroupChildList.map((e) => e.index).toList(); }); debugPrint( '2 -- firstGroupChildList -- ${model2.firstGroupChildList.map((e) => e.index)}'); debugPrint('2 -- displaying -- ${model2.displayingChildIndexList}'); } }, child: _buildGridView(), ), ); } Widget _buildGridView() { return CustomScrollView( slivers: [ _buildSliverGridView1(), _buildSliverGridView2(), ], ); } Widget _buildSliverGridView1() { return SliverGrid( delegate: SliverChildBuilderDelegate( (context, index) { if (_sliverGridViewContext1 != context) { _sliverGridViewContext1 = context; } return Container( color: (_hitIndexs1.contains(index)) ? Colors.red : Colors.blue[100], child: Center( child: Text('index -- $index'), ), ); }, childCount: 50, ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 2, mainAxisSpacing: 5, ), ); } Widget _buildSliverGridView2() { return SliverGrid( delegate: SliverChildBuilderDelegate( (context, index) { if (_sliverGridViewContext2 != context) { _sliverGridViewContext2 = context; } return Container( color: (_hitIndexs2.contains(index)) ? Colors.red : Colors.blue[100], child: Center( child: Text('index -- $index'), ), ); }, childCount: 50, ), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 140.0, childAspectRatio: 0.6, crossAxisSpacing: 2, mainAxisSpacing: 5, ), ); } } ================================================ FILE: example/lib/features/home/home_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/common/route/route.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("ScrollView Observer Example")), body: _buildListView(), ); } Widget _buildListView() { return ListView( children: [ ListTile( title: const Text("ListView"), onTap: () { NavigationService.push(MyPage.listView); }, ), ListTile( title: const Text("ListView - Context"), onTap: () { NavigationService.push(MyPage.listViewContext); }, ), ListTile( title: const Text("ListView - Fixed Height"), onTap: () { NavigationService.push(MyPage.listViewFixedHeight); }, ), ListTile( title: const Text("ListView - Horizontal"), onTap: () { NavigationService.push(MyPage.listViewHorizontal); }, ), ListTile( title: const Text("ListView - Dynamic Offset"), onTap: () { NavigationService.push(MyPage.listViewDynamicOffset); }, ), ListTile( title: const Text("ListView - Custom"), onTap: () { NavigationService.push(MyPage.listViewCustom); }, ), ListTile( title: const Text("ListView - Infinite"), onTap: () { NavigationService.push(MyPage.listViewInfinite); }, ), ListTile( title: const Text("SliverListView"), onTap: () { NavigationService.push(MyPage.sliverListView); }, ), ListTile( title: const Text("GridView"), onTap: () { NavigationService.push(MyPage.gridView); }, ), ListTile( title: const Text("GridView - Context"), onTap: () { NavigationService.push(MyPage.gridViewContext); }, ), ListTile( title: const Text("GridView - Fixed Height"), onTap: () { NavigationService.push(MyPage.gridViewFixedHeight); }, ), ListTile( title: const Text("GridView - Horizontal"), onTap: () { NavigationService.push(MyPage.gridViewHorizontal); }, ), ListTile( title: const Text("GridView - Custom"), onTap: () { NavigationService.push(MyPage.gridViewCustom); }, ), ListTile( title: const Text("SliverGridView"), onTap: () { NavigationService.push(MyPage.sliverGridView); }, ), ListTile( title: const Text("CustomScrollView"), onTap: () { NavigationService.push(MyPage.customScrollView); }, ), ListTile( title: const Text("CustomScrollView - Center"), onTap: () { NavigationService.push(MyPage.customScrollViewCenter); }, ), ListTile( title: const Text("MultiSliver"), onTap: () { NavigationService.push(MyPage.multiSliver); }, ), ListTile( title: const Text("SliverAppBar"), onTap: () { NavigationService.push(MyPage.sliverAppBar); }, ), ListTile( title: const Text("NestedScrollView"), onTap: () { NavigationService.push(MyPage.nestedScrollView); }, ), ListTile( title: const Text("NestedScrollView - TabBarView"), onTap: () { NavigationService.push(MyPage.nestedScrollViewTabBarView); }, ), ListTile( title: const Text("PageView"), onTap: () { NavigationService.push(MyPage.pageView); }, ), ListTile( title: const Text("PageView - Parallax"), onTap: () { NavigationService.push(MyPage.pageViewParallax); }, ), ListTile( title: const Text("PageView - Parallax ItemListener"), onTap: () { NavigationService.push(MyPage.pageViewParallaxItemListener); }, ), ListTile( title: const Text("VideoList AutoPlay"), onTap: () { NavigationService.push(MyPage.videoListAutoPlay); }, ), ListTile( title: const Text("AnchorList"), onTap: () { NavigationService.push(MyPage.anchorList); }, ), ListTile( title: const Text("AnchorWaterfall"), onTap: () { NavigationService.push(MyPage.anchorWaterfall); }, ), ListTile( title: const Text("ImageTab"), onTap: () { NavigationService.push(MyPage.imageTab); }, ), ListTile( title: const Text("Chat"), onTap: () { NavigationService.push(MyPage.chat); }, ), ListTile( title: const Text("ChatGPT"), onTap: () { NavigationService.push(MyPage.chatGPT); }, ), ListTile( title: const Text("Waterfall Flow"), onTap: () { NavigationService.push(MyPage.waterfallFlow); }, ), ListTile( title: const Text("Waterfall Flow - Fixed Height"), onTap: () { NavigationService.push(MyPage.waterfallFlowFixedHeight); }, ), ListTile( title: const Text("ScrollView Form"), onTap: () { NavigationService.push(MyPage.scrollViewForm); }, ), ListTile( title: const Text("Visibility ListView"), onTap: () { NavigationService.push(MyPage.visibilityListView); }, ), ListTile( title: const Text("Visibility ScrollView"), onTap: () { NavigationService.push(MyPage.visibilityScrollView); }, ), ListTile( title: const Text("AzList"), onTap: () { NavigationService.push(MyPage.azList); }, ), ListTile( title: const Text("Expandable Carousel Slider"), onTap: () { NavigationService.push(MyPage.expandableCarouselSlider); }, ), ListTile( title: const Text("Detail Page"), onTap: () { NavigationService.push(MyPage.detail); }, ), ], ); } } ================================================ FILE: example/lib/features/listview/horizontal_listview_demo/horizontal_listview_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; class HorizontalListViewPage extends StatefulWidget { const HorizontalListViewPage({Key? key}) : super(key: key); @override State createState() => _HorizontalListViewPageState(); } class _HorizontalListViewPageState extends State { BuildContext? _sliverListViewContext; int _hitIndex = 0; @override void initState() { super.initState(); // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { ListViewOnceObserveNotification().dispatch(_sliverListViewContext); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("ListView")), body: ListViewObserver( child: _buildListView(), sliverListContexts: () { return [if (_sliverListViewContext != null) _sliverListViewContext!]; }, onObserveAll: (resultMap) { final model = resultMap[_sliverListViewContext]; if (model == null) return; debugPrint('firstChild.index -- ${model.firstChild?.index ?? 0}'); debugPrint('displaying -- ${model.displayingChildIndexList}'); setState(() { _hitIndex = model.firstChild?.index ?? 0; }); }, ), ); } ListView _buildListView() { return ListView.separated( itemBuilder: (ctx, index) { if (_sliverListViewContext != ctx) { _sliverListViewContext = ctx; } return _buildListItemView(index); }, separatorBuilder: (ctx, index) { return _buildSeparatorView(); }, itemCount: 50, scrollDirection: Axis.horizontal, ); } Widget _buildListItemView(int index) { return Container( width: (index % 2 == 0) ? 180 : 150, color: _hitIndex == index ? Colors.red : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: _hitIndex == index ? Colors.white : Colors.black, ), ), ), ); } Container _buildSeparatorView() { return Container( color: Colors.white, width: 5, ); } } ================================================ FILE: example/lib/features/listview/infinite_listview_demo/infinite_listview_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-12-31 14:00:51 */ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; class InfiniteListViewPage extends StatefulWidget { const InfiniteListViewPage({Key? key}) : super(key: key); @override State createState() => _InfiniteListViewPageState(); } class _InfiniteListViewPageState extends State { List dataSource = []; bool isLoadingForHeader = false; bool isLoadingForFooter = false; final double itemMinHeight = 50; final double itemMaxHeight = 150; final Random _random = Random(); double get randomItemHeight => itemMinHeight + _random.nextInt((itemMaxHeight - itemMinHeight).toInt()); List itemHeights = []; int generateCount = 20; ScrollController scrollController = ScrollController(); late ListObserverController observerController; late ChatScrollObserver chatObserver; handleLoadMoreForHeader() async { if (isLoadingForHeader) return; // loading isLoadingForHeader = true; int initIndex = 0; if (dataSource.isNotEmpty) { initIndex = dataSource.first; } await Future.delayed(const Duration(milliseconds: 100)); dataSource.insertAll( 0, List.generate(generateCount, (index) { return initIndex - (generateCount - index); }), ); itemHeights.insertAll( 0, List.generate(generateCount, (_) => randomItemHeight), ); // Keeping position chatObserver.standby(changeCount: generateCount); // Clearing the offset cache observerController.clearScrollIndexCache(); setState(() {}); isLoadingForHeader = false; } handleLoadMoreForFooter() async { if (isLoadingForFooter) return; // loading isLoadingForFooter = true; int initIndex = 0; if (dataSource.isNotEmpty) { initIndex = dataSource.last; } await Future.delayed(const Duration(milliseconds: 100)); setState(() { dataSource.addAll( List.generate(generateCount, (index) => index + initIndex + 1), ); itemHeights.addAll( List.generate(generateCount, (_) => randomItemHeight), ); }); isLoadingForFooter = false; } @override void initState() { super.initState(); observerController = ListObserverController(controller: scrollController) ..initialIndex = generateCount ~/ 2; chatObserver = ChatScrollObserver(observerController) ..fixedPositionOffset = -double.maxFinite ..toRebuildScrollViewCallback = () { setState(() {}); }; dataSource = List.generate(generateCount, (index) => index); itemHeights = List.generate(generateCount, (_) => randomItemHeight); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("ListView")), body: ListViewObserver( controller: observerController, triggerOnObserveType: ObserverTriggerOnObserveType.directly, child: _buildListView(), onObserve: (result) { debugPrint( 'displaying -- ${result.displayingChildIndexList.map((i) => dataSource[i])}'); if (result.firstChild?.index == 0) { final firstChildLeadingMarginToViewport = result.firstChild?.leadingMarginToViewport ?? 0; if (firstChildLeadingMarginToViewport > -100) { handleLoadMoreForHeader(); } } else if (result.displayingChildIndexList.last == dataSource.length - 1) { final lastChildTrailingMarginToViewport = result.displayingChildModelList.last.trailingMarginToViewport; if (lastChildTrailingMarginToViewport < 100) { handleLoadMoreForFooter(); } } }, ), ); } Widget _buildListView() { return ListView.separated( physics: ChatObserverClampingScrollPhysics(observer: chatObserver), padding: EdgeInsets.zero, controller: scrollController, itemBuilder: (ctx, index) { return _buildListItemView(index); }, separatorBuilder: (ctx, index) { return _buildSeparatorView(); }, itemCount: dataSource.length, // Ensure that the reference item can be found when keeping position. cacheExtent: itemMaxHeight * (generateCount + 2), ); } Widget _buildListItemView(int index) { return Container( height: itemHeights[index], color: index % 2 == 0 ? Colors.black26 : Colors.black12, child: Center( child: Text( "index -- ${dataSource[index]}", style: const TextStyle( color: Colors.black, ), ), ), ); } Widget _buildSeparatorView() { return Container( color: Colors.white, width: 5, ); } } ================================================ FILE: example/lib/features/listview/listview_ctx_demo/listview_ctx_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; class ListViewCtxDemoPage extends StatefulWidget { const ListViewCtxDemoPage({Key? key}) : super(key: key); @override State createState() => _ListViewCtxDemoPageState(); } class _ListViewCtxDemoPageState extends State { BuildContext? _sliverListViewContext; int _hitIndex = 0; @override void initState() { super.initState(); // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { ListViewOnceObserveNotification().dispatch(_sliverListViewContext); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("ListView")), body: ListViewObserver( child: _buildListView(), sliverListContexts: () { return [if (_sliverListViewContext != null) _sliverListViewContext!]; }, onObserveAll: (resultMap) { final model = resultMap[_sliverListViewContext]; if (model == null) return; debugPrint('visible -- ${model.visible}'); debugPrint('firstChild.index -- ${model.firstChild?.index}'); debugPrint('displaying -- ${model.displayingChildIndexList}'); setState(() { _hitIndex = model.firstChild?.index ?? 0; }); }, ), ); } ListView _buildListView() { // return ListView.builder( // padding: EdgeInsets.zero, // itemCount: 200, // itemBuilder: (ctx, index) { // if (_sliverListViewContext != ctx) { // _sliverListViewContext = ctx; // } // return _buildListItemView(index); // }, // ); return ListView.separated( padding: const EdgeInsets.only(top: 1000, bottom: 1000), controller: ScrollController(initialScrollOffset: 1000), itemBuilder: (ctx, index) { if (_sliverListViewContext != ctx) { _sliverListViewContext = ctx; } return _buildListItemView(index); }, separatorBuilder: (ctx, index) { return _buildSeparatorView(); }, itemCount: 50, ); } Widget _buildListItemView(int index) { return Container( height: (index % 2 == 0) ? 80 : 50, color: _hitIndex == index ? Colors.red : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: _hitIndex == index ? Colors.white : Colors.black, ), ), ), ); } Container _buildSeparatorView() { return Container( color: Colors.white, height: 5, ); } } ================================================ FILE: example/lib/features/listview/listview_custom_demo/listview_custom_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-05-21 10:31:44 */ // ignore: implementation_imports import 'package:extended_list/src/rendering/sliver_list.dart'; import 'package:flutter/material.dart'; import 'package:loading_more_list/loading_more_list.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; class ListViewCustomDemoPage extends StatefulWidget { const ListViewCustomDemoPage({Key? key}) : super(key: key); @override State createState() => _ListViewCustomDemoPageState(); } class _ListViewCustomDemoPageState extends State { ScrollController scrollController = ScrollController(); late ListObserverController observerController; @override void initState() { observerController = ListObserverController( controller: scrollController, ); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Custom')), body: ListViewObserver( child: _buildListView(), controller: observerController, customTargetRenderSliverType: (renderObj) { // Here you tell the package what type of RenderObject it needs to observe. return renderObj is ExtendedRenderSliverList; }, // customHandleObserve: (context) { // // Here you can customize the observation logic. // return ObserverCore.handleListObserve( // context: context, // fetchLeadingOffset: () => 100, // ); // }, onObserve: (resultModel) { debugPrint('firstChild.index -- ${resultModel.firstChild?.index}'); debugPrint('displaying -- ${resultModel.displayingChildIndexList}'); }, ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.airline_stops_sharp), onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'Jump to row 10', ); observerController.jumpTo( index: 10, isFixedHeight: true, ); }, ), ); } Widget _buildListView() { return LoadingMoreList( ListConfig( controller: scrollController, itemBuilder: (context, item, index) { if (scrollController.hasClients && (observerController.sliverContexts.isEmpty || observerController.sliverContexts.first != context)) { observerController.reattach(); } return ListTile( title: Text('index - $index'), ); }, sourceList: SourceList(), ), ); } } class SourceList extends LoadingMoreBase { @override Future loadData([bool isloadMoreAction = false]) async { await Future.delayed(const Duration(seconds: 2)); for (var i = 0; i < 30; i++) { add(i); } return true; } } ================================================ FILE: example/lib/features/listview/listview_demo/listview_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; class ListViewDemoPage extends StatefulWidget { const ListViewDemoPage({Key? key}) : super(key: key); @override State createState() => _ListViewDemoPageState(); } class _ListViewDemoPageState extends State { static const double _leadingPadding = 1000; static const double _trailingPadding = 2000; static const EdgeInsets _padding = EdgeInsets.only( top: _leadingPadding, bottom: _trailingPadding, ); int _hitIndex = 0; ScrollController scrollController = ScrollController(initialScrollOffset: _leadingPadding); late ListObserverController observerController; @override void initState() { super.initState(); observerController = ListObserverController(controller: scrollController) ..initialIndexModel = ObserverIndexPositionModel( index: 10, padding: _padding, ); // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.endOfFrame.then( (_) { // After layout if (mounted) { observerController.dispatchOnceObserve(); } }, ); } @override void dispose() { observerController.controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("ListView")), body: ListViewObserver( child: _buildListView(), autoTriggerObserveTypes: const [ ObserverAutoTriggerObserveType.scrollEnd, ], triggerOnObserveType: ObserverTriggerOnObserveType.directly, controller: observerController, onObserve: (resultModel) { debugPrint('visible -- ${resultModel.visible}'); debugPrint('firstChild.index -- ${resultModel.firstChild?.index}'); debugPrint('displaying -- ${resultModel.displayingChildIndexList}'); for (var item in resultModel.displayingChildModelList) { debugPrint( 'item - ${item.index} - ${item.leadingMarginToViewport} - ${item.trailingMarginToViewport}'); } setState(() { _hitIndex = resultModel.firstChild?.index ?? 0; }); }, ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.airline_stops_outlined), onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'Jump to row 50', ); observerController.jumpTo( index: 50, padding: _padding, ); // observerController.animateTo( // index: 50, // duration: const Duration(seconds: 1), // curve: Curves.ease, // padding: _padding, // ); }, ), ); } ListView _buildListView() { // return ListView.builder( // itemExtent: 50, // controller: scrollController, // itemCount: 100, // itemBuilder: (context, index) { // return _buildListItemView(index); // }, // ); return ListView.separated( padding: _padding, controller: scrollController, itemBuilder: (ctx, index) { return _buildListItemView(index); }, separatorBuilder: (ctx, index) { return _buildSeparatorView(); }, itemCount: 100, cacheExtent: double.maxFinite, ); } Widget _buildListItemView(int index) { return Container( height: (index % 2 == 0) ? 80 : 50, color: _hitIndex == index ? Colors.red : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: _hitIndex == index ? Colors.white : Colors.black, ), ), ), ); } Container _buildSeparatorView() { return Container( color: Colors.white, height: 5, ); } } ================================================ FILE: example/lib/features/listview/listview_dynamic_offset/listview_dynamic_offset_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; class ListViewDynamicOffsetPage extends StatefulWidget { const ListViewDynamicOffsetPage({Key? key}) : super(key: key); @override State createState() => _ListViewDynamicOffsetPageState(); } class _ListViewDynamicOffsetPageState extends State { BuildContext? _sliverListViewContext; int _hitIndex = 0; double _safeAreaPaddingTop = 0; final double _navContentHeight = 44; double _navBgAlpha = 0; bool _isShowNavTitle = false; final ScrollController _pageController = ScrollController(); @override void initState() { super.initState(); _pageController.addListener(_pageDidScroll); ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { _safeAreaPaddingTop = MediaQuery.of(context).padding.top; // Trigger an observation manually ListViewOnceObserveNotification().dispatch(_sliverListViewContext); }); } @override void dispose() { _pageController.removeListener(_pageDidScroll); _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ ListViewObserver( child: _buildListView(), sliverListContexts: () { return [ if (_sliverListViewContext != null) _sliverListViewContext! ]; }, dynamicLeadingOffset: () { if (_navBgAlpha < 1) { return 0; } return _safeAreaPaddingTop + _navContentHeight; }, onObserveAll: (resultMap) { final model = resultMap[_sliverListViewContext]; if (model == null) return; debugPrint('firstChild.index -- ${model.firstChild?.index}'); debugPrint('displaying -- ${model.displayingChildIndexList}'); setState(() { _hitIndex = model.firstChild?.index ?? 0; }); }, ), Container( color: Colors.grey.withValues(alpha: _navBgAlpha), child: Column( mainAxisSize: MainAxisSize.min, children: [ SafeArea( bottom: false, child: _buildNavContentWidget(context), ) ], ), ), ], ), ); } ListView _buildListView() { return ListView.separated( controller: _pageController, padding: EdgeInsets.zero, itemBuilder: (ctx, index) { if (_sliverListViewContext != ctx) { _sliverListViewContext = ctx; } return _buildListItemView(index); }, separatorBuilder: (ctx, index) { return _buildSeparatorView(); }, itemCount: 50, ); } Widget _buildListItemView(int index) { return Container( height: (index % 2 == 0) ? 80 : 50, color: _hitIndex == index ? Colors.red : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: _hitIndex == index ? Colors.white : Colors.black, ), ), ), ); } Container _buildSeparatorView() { return Container( color: Colors.white, height: 5, ); } Widget _buildNavContentWidget(BuildContext context) { return Container( height: _navContentHeight, padding: const EdgeInsets.symmetric(horizontal: 15), child: Row( children: [ GestureDetector( child: Icon( Icons.arrow_back_ios, color: _isShowNavTitle ? Colors.black : Colors.white, ), onTap: () { Navigator.of(context).pop(); }, ), Expanded( child: Center( child: Text( "ListView Dynnamic Offset", style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold, color: Colors.black54.withValues( alpha: _isShowNavTitle ? 1 : 0, ), ), ), ), ), ], ), ); } void _pageDidScroll() { final offset = _pageController.offset; double _newNavBgAlpha = 0; if (offset < 0) { _newNavBgAlpha = 0; } else if (offset >= _navContentHeight) { _newNavBgAlpha = 1; } else { _newNavBgAlpha = offset / _navContentHeight; } if (_navBgAlpha != _newNavBgAlpha) { setState(() { _navBgAlpha = _newNavBgAlpha; _isShowNavTitle = _navBgAlpha > .5; }); } } } ================================================ FILE: example/lib/features/listview/listview_fixed_height_demo/listview_fixed_height_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; class ListViewFixedHeightDemoPage extends StatefulWidget { const ListViewFixedHeightDemoPage({Key? key}) : super(key: key); @override State createState() => _ListViewFixedHeightDemoPageState(); } class _ListViewFixedHeightDemoPageState extends State { int _hitIndex = 0; ScrollController scrollController = ScrollController(initialScrollOffset: 1000); late ListObserverController observerController; @override void initState() { super.initState(); observerController = ListObserverController(controller: scrollController) ..initialIndexModel = ObserverIndexPositionModel( index: 3, isFixedHeight: true, ); // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.endOfFrame.then( (_) { // After layout if (mounted) { observerController.dispatchOnceObserve(); } }, ); } @override void dispose() { observerController.controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("ListView")), body: ListViewObserver( child: _buildListView(), controller: observerController, onObserve: (resultModel) { // print('visible -- ${resultModel.visible}'); // print('firstChild.index -- ${resultModel.firstChild?.index}'); // print('displaying -- ${resultModel.displayingChildIndexList}'); // print( // 'leadingAxisMarginToViewport1 -- ${resultModel.firstChild?.leadingAxisMarginToViewport}'); // print( // 'leadingAxisMarginToViewport2 -- ${resultModel.displayingChildModelList.last.leadingAxisMarginToViewport}'); // print( // 'trailingAxisMarginToViewport1 -- ${resultModel.firstChild?.trailingAxisMarginToViewport}'); // print( // 'trailingAxisMarginToViewport2 -- ${resultModel.displayingChildModelList.last.trailingAxisMarginToViewport}'); for (var item in resultModel.displayingChildModelList) { debugPrint( 'item - ${item.index} - ${item.leadingMarginToViewport} - ${item.trailingMarginToViewport}'); } setState(() { _hitIndex = resultModel.firstChild?.index ?? 0; }); }, ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.airline_stops_outlined), onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'Jump to row 100', ); observerController.jumpTo(index: 100, isFixedHeight: true); // observerController.animateTo( // index: 100, // isFixedHeight: true, // duration: const Duration(seconds: 1), // curve: Curves.ease, // ); }, ), ); } ListView _buildListView() { return ListView.separated( padding: const EdgeInsets.only(top: 1000, bottom: 1000), controller: scrollController, itemBuilder: (ctx, index) { return _buildListItemView(index); }, separatorBuilder: (ctx, index) { return _buildSeparatorView(); }, itemCount: 500, ); } Widget _buildListItemView(int index) { return Container( height: 80, color: _hitIndex == index ? Colors.red : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: _hitIndex == index ? Colors.white : Colors.black, ), ), ), ); } Container _buildSeparatorView() { return Container( color: Colors.white, height: 5, ); } } ================================================ FILE: example/lib/features/listview/sliver_list_demo/sliver_list_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/typedefs.dart'; class SliverListViewDemoPage extends StatefulWidget { const SliverListViewDemoPage({Key? key}) : super(key: key); @override State createState() => _SliverListViewDemoPageState(); } class _SliverListViewDemoPageState extends State { BuildContext? _sliverListViewCtx1; final GlobalKey _sliverListView2Key = GlobalKey(); BuildContext? get _sliverListViewCtx2 => _sliverListView2Key.currentContext; int _hitIndexForCtx1 = 0; int _hitIndexForCtx2 = 0; @override void initState() { super.initState(); // Trigger an observation manually ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { ListViewOnceObserveNotification().dispatch(_sliverListViewCtx1); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("SliverListView")), body: ListViewObserver( child: CustomScrollView( slivers: [ _buildSliverListView1(), _buildSliverListView2(), ], ), sliverListContexts: () { return [ if (_sliverListViewCtx1 != null) _sliverListViewCtx1!, if (_sliverListViewCtx2 != null) _sliverListViewCtx2!, ]; }, onObserveAll: (resultMap) { final model1 = resultMap[_sliverListViewCtx1]; if (model1 != null && model1.visible) { debugPrint('1 visible -- ${model1.visible}'); debugPrint('1 firstChild.index -- ${model1.firstChild?.index}'); debugPrint('1 displaying -- ${model1.displayingChildIndexList}'); setState(() { _hitIndexForCtx1 = model1.firstChild?.index ?? 0; }); } final model2 = resultMap[_sliverListViewCtx2]; if (model2 != null && model2.visible) { debugPrint('2 visible -- ${model2.visible}'); debugPrint('2 firstChild.index -- ${model2.firstChild?.index}'); debugPrint('2 displaying -- ${model2.displayingChildIndexList}'); setState(() { _hitIndexForCtx2 = model2.firstChild?.index ?? 0; }); } }, ), ); } SliverList _buildSliverListView1() { return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { if (_sliverListViewCtx1 != ctx) { _sliverListViewCtx1 = ctx; } return Container( height: (index % 2 == 0) ? 80 : 50, color: _hitIndexForCtx1 == index ? Colors.red : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: _hitIndexForCtx1 == index ? Colors.white : Colors.black, ), ), ), ); }, childCount: 30, ), ); } SliverList _buildSliverListView2() { return SliverList( key: _sliverListView2Key, delegate: SliverChildBuilderDelegate( (ctx, index) { return Container( height: (index % 2 == 0) ? 80 : 50, color: _hitIndexForCtx2 == index ? Colors.amber : Colors.blue[50], child: Center( child: Text( "index -- $index", style: TextStyle( color: _hitIndexForCtx2 == index ? Colors.white : Colors.black, ), ), ), ); }, childCount: 30, ), ); } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_demo/nested_scrollview_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-11-27 22:05:28 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; import 'package:scrollview_observer_example/widgets/sliver.dart'; class NestedScrollViewDemoPage extends StatefulWidget { const NestedScrollViewDemoPage({Key? key}) : super(key: key); @override State createState() => _NestedScrollViewDemoPageState(); } class _NestedScrollViewDemoPageState extends State with TickerProviderStateMixin { BuildContext? _sliverHeaderListCtx; BuildContext? _sliverBodyListCtx; BuildContext? _sliverBodyGridCtx; GlobalKey nestedScrollViewKey = GlobalKey(); GlobalKey appBarKey = GlobalKey(); GlobalKey tabBarKey = GlobalKey(); final nestedScrollUtil = NestedScrollUtil(); int _hitIndexForListCtx = 0; List _hitIndexesForGrid = []; ScrollController outerScrollController = ScrollController(); ScrollController? bodyScrollController; late SliverObserverController observerController = SliverObserverController( controller: outerScrollController, ); late TabController tabBarController = TabController( length: 3, vsync: this, ); bool scrollToWithAnimation = false; @override void initState() { super.initState(); nestedScrollUtil.outerScrollController = outerScrollController; } @override void dispose() { outerScrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: SliverViewObserver( controller: observerController, child: _buildNestedScrollView(), sliverContexts: () { return [ if (_sliverHeaderListCtx != null) _sliverHeaderListCtx!, if (_sliverBodyListCtx != null) _sliverBodyListCtx!, if (_sliverBodyGridCtx != null) _sliverBodyGridCtx!, ]; }, customOverlap: (sliverContext) { return nestedScrollUtil.calcOverlap( nestedScrollViewKey: nestedScrollViewKey, sliverContext: sliverContext, ); }, onObserveAll: (result) { result.forEach((key, value) { if (key == _sliverHeaderListCtx) { debugPrint( "SliverListHeaderCtx: ${value.displayingChildIndexList}"); } else if (key == _sliverBodyListCtx) { final model = value as ListViewObserveModel; debugPrint("SliverListCtx: ${model.displayingChildIndexList}"); if (_hitIndexForListCtx != model.firstChild?.index) { _hitIndexForListCtx = model.firstChild?.index ?? 0; setState(() {}); } } else if (key == _sliverBodyGridCtx) { final model = value as GridViewObserveModel; debugPrint("SliverGridCtx: ${model.displayingChildIndexList}"); final firstGroupChildIndexList = model.firstGroupChildList.map((e) => e.index).toList(); if (_hitIndexesForGrid != firstGroupChildIndexList) { _hitIndexesForGrid = firstGroupChildIndexList; setState(() {}); } } }); }, ), floatingActionButton: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( icon: const Icon(Icons.list_alt_rounded), onPressed: () { scrollTo( position: NestedScrollUtilPosition.header, index: 1, sliverContext: _sliverHeaderListCtx, ); SnackBarUtil.showSnackBar( context: context, text: 'Header - SliverList - Scrolling to item 1', ); }, ), IconButton( icon: const Icon(Icons.list_rounded), onPressed: () { scrollTo( position: NestedScrollUtilPosition.body, index: 1, sliverContext: _sliverBodyListCtx, ); SnackBarUtil.showSnackBar( context: context, text: 'Body - SliverList - Scrolling to item 1', ); }, ), IconButton( icon: const Icon(Icons.grid_view), onPressed: () { scrollTo( position: NestedScrollUtilPosition.body, index: 5, sliverContext: _sliverBodyGridCtx, ); SnackBarUtil.showSnackBar( context: context, text: 'Body - SliverGrid - Scrolling to item 5', ); }, ), IconButton( icon: const Icon(Icons.restore_outlined), onPressed: resetAllSliverObservationData, ) ], ), ); } Widget _buildNestedScrollView() { return NestedScrollView( key: nestedScrollViewKey, controller: outerScrollController, headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverAppBar( key: appBarKey, title: const Text("NestedScrollView"), pinned: true, forceElevated: innerBoxIsScrolled, actions: [ Switch( value: scrollToWithAnimation, onChanged: ((value) { scrollToWithAnimation = value; setState(() {}); SnackBarUtil.showSnackBar( context: context, text: "Scroll to with animation: $scrollToWithAnimation", ); }), ), ], ), SliverFixedExtentList( delegate: SliverChildBuilderDelegate( (ctx, index) { if (_sliverHeaderListCtx != ctx) { _sliverHeaderListCtx = ctx; nestedScrollUtil.headerSliverContexts.add(ctx); } return ListTile( leading: Text("Item $index"), ); }, childCount: 5, ), itemExtent: 50, ), SliverPersistentHeader( key: tabBarKey, pinned: true, delegate: SliverHeaderDelegate.fixedHeight( height: 40, child: Container( color: Colors.blue, child: TabBar( controller: tabBarController, tabs: const [ Tab(text: "Tab 1"), Tab(text: "Tab 2"), Tab(text: "Tab 3"), ], ), ), ), ), ]; }, body: Builder(builder: (context) { // Get the inner scroll controller. final innerScrollController = PrimaryScrollController.of(context); if (nestedScrollUtil.bodyScrollController != innerScrollController) { nestedScrollUtil.bodyScrollController = innerScrollController; } return CustomScrollView( slivers: [ _buildSliverListView(), _buildSliverGridView(), ], ); }), ); } Widget _buildSliverListView() { return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { if (_sliverBodyListCtx != ctx) { _sliverBodyListCtx = ctx; nestedScrollUtil.bodySliverContexts.add(ctx); } return Container( height: (index % 2 == 0) ? 80 : 50, color: _hitIndexForListCtx == index ? Colors.red : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: _hitIndexForListCtx == index ? Colors.white : Colors.black, ), ), ), ); }, childCount: 30, ), ); } Widget _buildSliverGridView() { return SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 2.0, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { if (_sliverBodyGridCtx != context) { _sliverBodyGridCtx = context; nestedScrollUtil.bodySliverContexts.add(context); } return Container( color: (_hitIndexesForGrid.contains(index)) ? Colors.green : Colors.blue[100], child: Center( child: Text('index -- $index'), ), ); }, childCount: 150, ), ); } double calcPersistentHeaderExtent( double offset, { required bool isBody, }) { double value = ObserverUtils.calcPersistentHeaderExtent( key: appBarKey, offset: offset, ); if (isBody) { value += ObserverUtils.calcPersistentHeaderExtent( key: tabBarKey, offset: offset, ); } return value; } resetAllSliverObservationData() { nestedScrollUtil.reset(); nestedScrollViewKey = GlobalKey(); nestedScrollUtil.outerScrollController = outerScrollController; setState(() {}); SnackBarUtil.showSnackBar( context: context, text: "Reset all sliver's observation data\n", ); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { observerController.reattach(); }); } scrollTo({ required NestedScrollUtilPosition position, required int index, required BuildContext? sliverContext, }) { bool isBody = NestedScrollUtilPosition.body == position; if (scrollToWithAnimation) { nestedScrollUtil.animateTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, position: position, index: index, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, sliverContext: sliverContext, offset: (targetOffset) { return calcPersistentHeaderExtent( targetOffset, isBody: isBody, ); }, ); } else { nestedScrollUtil.jumpTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, position: position, index: index, sliverContext: sliverContext, offset: (targetOffset) { return calcPersistentHeaderExtent( targetOffset, isBody: isBody, ); }, ); } } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-23 22:19:39 */ import 'package:flutter/material.dart'; import 'package:getx_helper/getx_helper.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; typedef NestedScrollviewTabBarViewDemoLogicPutMixin = GetxLogicPutStateMixin; typedef NestedScrollviewTabBarViewDemoLogicConsumerMixin< W extends StatefulWidget> = GetxLogicConsumerStateMixin; enum NestedScrollviewTabBarViewDemoUpdateType { scrollTypeSwitch, floatingActionButton, headerSliverList, tab1View, tab2View, tab3ViewSliverList, tab3ViewSliverGrid, } enum NestedScrollviewTabBarViewDemoTabType { tab1(title: "List"), tab2(title: "Grid"), tab3(title: "List+Grid"); const NestedScrollviewTabBarViewDemoTabType({ required this.title, }); final String title; } enum NestedScrollviewTabBarViewDemoFABClickType { headerSliverList, tab1SliverList, tab2SliverGrid, tab3SliverList, tab3SliverGrid, } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-23 22:19:39 */ import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_tab_bar.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/state/nested_scrollview_tab_bar_view_demo_state.dart'; class NestedScrollViewTabBarViewDemoLogic extends GetxController with GetTickerProviderStateMixin { final NestedScrollViewTabBarViewDemoState state = NestedScrollViewTabBarViewDemoState(); @override void onInit() async { super.onInit(); onInitForTabBar(); } void onDispose() { onDisposeForTabBar(); state.outerScrollController.dispose(); } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_floating_action_btn.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 23:20:17 */ import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_observer.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; extension NestedScrollViewTabBarViewDemoLogicForFAB on NestedScrollViewTabBarViewDemoLogic { void handleFABClick( NestedScrollviewTabBarViewDemoFABClickType type, ) { final context = state.rootContext; if (context == null) return; switch (type) { case NestedScrollviewTabBarViewDemoFABClickType.headerSliverList: scrollTo( position: NestedScrollUtilPosition.header, index: 1, sliverContext: state.headerSliverListCtx, ); SnackBarUtil.showSnackBar( context: context, text: 'Header - SliverList - Scrolling to item 1', ); break; case NestedScrollviewTabBarViewDemoFABClickType.tab1SliverList: scrollTo( position: NestedScrollUtilPosition.body, index: 1, sliverContext: state.sliverTab1ListCtx, ); SnackBarUtil.showSnackBar( context: context, text: 'Body - Tab1 SliverList - Scrolling to item 1', ); break; case NestedScrollviewTabBarViewDemoFABClickType.tab2SliverGrid: scrollTo( position: NestedScrollUtilPosition.body, index: 1, sliverContext: state.tab2SliverGridCtx, ); SnackBarUtil.showSnackBar( context: context, text: 'Body - Tab2 SliverGrid - Scrolling to item 1', ); break; case NestedScrollviewTabBarViewDemoFABClickType.tab3SliverList: scrollTo( position: NestedScrollUtilPosition.body, index: 1, sliverContext: state.tab3SliverListCtx, ); SnackBarUtil.showSnackBar( context: context, text: 'Body - Tab3 SliverList - Scrolling to item 1', ); break; case NestedScrollviewTabBarViewDemoFABClickType.tab3SliverGrid: scrollTo( position: NestedScrollUtilPosition.body, index: 1, sliverContext: state.tab3SliverGridCtx, ); SnackBarUtil.showSnackBar( context: context, text: 'Body - Tab3 SliverGrid - Scrolling to item 1', ); break; } } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_observer.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 23:16:49 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; extension NestedScrollViewTabBarViewDemoLogicForObserver on NestedScrollViewTabBarViewDemoLogic { void updateNestedScrollUtilHeaderSliverContextsIfNeed({ required BuildContext? oldCtx, required BuildContext newCtx, required Function() toRecordCtx, }) { if (oldCtx == newCtx) return; state.nestedScrollUtil.headerSliverContexts.remove( oldCtx, ); toRecordCtx.call(); state.nestedScrollUtil.headerSliverContexts.add(newCtx); } void updateNestedScrollUtilBodySliverContextsIfNeed({ required BuildContext? oldCtx, required BuildContext newCtx, required Function() toRecordCtx, }) { if (oldCtx == newCtx) return; state.nestedScrollUtil.bodySliverContexts.remove( oldCtx, ); toRecordCtx.call(); state.nestedScrollUtil.bodySliverContexts.add(newCtx); } void handleOnObserveAll(Map resultMap) { resultMap.forEach((key, value) { // Header sliver list if (key == state.headerSliverListCtx) { final model = value as ListViewObserveModel; debugPrint("SliverListHeaderCtx: ${model.displayingChildIndexList}"); if (state.hitIndexForHeaderListCtx == model.firstChild?.index) return; state.hitIndexForHeaderListCtx = model.firstChild?.index ?? 0; update([ NestedScrollviewTabBarViewDemoUpdateType.headerSliverList, ]); return; } // Tab1 sliver list if (key == state.sliverTab1ListCtx) { final model = value as ListViewObserveModel; debugPrint("sliverTab1ListCtx: ${model.displayingChildIndexList}"); if (state.hitIndexForTab1ListCtx == model.firstChild?.index) return; state.hitIndexForTab1ListCtx = model.firstChild?.index ?? 0; update([ NestedScrollviewTabBarViewDemoUpdateType.tab1View, ]); return; } // Tab2 sliver grid if (key == state.tab2SliverGridCtx) { final model = value as GridViewObserveModel; debugPrint("sliverTab2GridCtx: ${model.displayingChildIndexList}"); final firstGroupChildIndexList = model.firstGroupChildList.map((e) => e.index).toList(); if (state.hitIndexesForTab2Grid == firstGroupChildIndexList) return; state.hitIndexesForTab2Grid = firstGroupChildIndexList; update([ NestedScrollviewTabBarViewDemoUpdateType.tab2View, ]); return; } // Tab3 sliver list if (key == state.tab3SliverListCtx) { final model = value as ListViewObserveModel; debugPrint("sliverTab3ListCtx: ${model.displayingChildIndexList}"); if (state.hitIndexForTab3ListCtx == model.firstChild?.index) return; state.hitIndexForTab3ListCtx = model.firstChild?.index ?? 0; update([ NestedScrollviewTabBarViewDemoUpdateType.tab3ViewSliverList, ]); return; } // Tab3 sliver grid if (key == state.tab3SliverGridCtx) { final model = value as GridViewObserveModel; debugPrint("sliverTab3GridCtx: ${model.displayingChildIndexList}"); final firstGroupChildIndexList = model.firstGroupChildList.map((e) => e.index).toList(); if (state.hitIndexesForTab3Grid == firstGroupChildIndexList) return; state.hitIndexesForTab3Grid = firstGroupChildIndexList; update([ NestedScrollviewTabBarViewDemoUpdateType.tab3ViewSliverGrid, ]); return; } }); } void scrollTo({ required NestedScrollUtilPosition position, required int index, required BuildContext? sliverContext, }) { bool isBody = NestedScrollUtilPosition.body == position; if (state.scrollToWithAnimation) { state.nestedScrollUtil.animateTo( nestedScrollViewKey: state.nestedScrollViewKey, observerController: state.observerController, position: position, index: index, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, sliverContext: sliverContext, offset: (targetOffset) { return calcPersistentHeaderExtent( targetOffset, isBody: isBody, ); }, ); } else { state.nestedScrollUtil.jumpTo( nestedScrollViewKey: state.nestedScrollViewKey, observerController: state.observerController, position: position, index: index, sliverContext: sliverContext, offset: (targetOffset) { return calcPersistentHeaderExtent( targetOffset, isBody: isBody, ); }, ); } } double calcPersistentHeaderExtent( double offset, { required bool isBody, }) { double value = ObserverUtils.calcPersistentHeaderExtent( key: state.appBarKey, offset: offset, ); if (isBody) { value += ObserverUtils.calcPersistentHeaderExtent( key: state.tabBarKey, offset: offset, ); } return value; } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_scroll_type_switch.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 23:20:17 */ import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; extension NestedScrollViewTabBarViewDemoLogicForScrollTypeSwitch on NestedScrollViewTabBarViewDemoLogic { void handleScrollTypeSwitchOnChanged(bool value) async { final context = state.rootContext; if (context == null) return; state.scrollToWithAnimation = value; update([ NestedScrollviewTabBarViewDemoUpdateType.scrollTypeSwitch, ]); SnackBarUtil.showSnackBar( context: context, text: "Animated scrolling ${state.scrollToWithAnimation ? 'Enabled' : 'Disabled'}", ); } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_tab_bar.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 23:14:47 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; extension NestedScrollViewTabBarViewDemoLogicForTabBar on NestedScrollViewTabBarViewDemoLogic { void onInitForTabBar() async { state.tabController = TabController( length: state.tabTypeList.length, vsync: this, ); state.tabController.addListener(() { if (state.tabController.indexIsChanging) return; update([ NestedScrollviewTabBarViewDemoUpdateType.floatingActionButton, ]); }); } void onDisposeForTabBar() { state.tabController.dispose(); } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/page/nested_scrollview_tab_bar_view_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-23 22:19:39 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_observer.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/state/nested_scrollview_tab_bar_view_demo_state.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_floating_action_btn.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_header_list_sliver.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_scroll_type_switch.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_tab1_view.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_tab2_view.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_tab3_view.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_tabbar.dart'; import 'package:scrollview_observer_example/widgets/sliver.dart'; class NestedScrollViewTabBarViewDemoPage extends StatefulWidget { const NestedScrollViewTabBarViewDemoPage({super.key}); @override State createState() => NestedScrollViewTabBarViewDemoPageState(); } class NestedScrollViewTabBarViewDemoPageState extends State with NestedScrollviewTabBarViewDemoLogicPutMixin< NestedScrollViewTabBarViewDemoPage>, TickerProviderStateMixin { NestedScrollViewTabBarViewDemoState get state => logic.state; NestedScrollUtil get nestedScrollUtil => state.nestedScrollUtil; @override void dispose() { logic.onDispose(); super.dispose(); } @override NestedScrollViewTabBarViewDemoLogic initLogic() => NestedScrollViewTabBarViewDemoLogic(); @override Widget buildBody(BuildContext context) { state.rootContext = context; Widget resultWidget = GetBuilder( tag: logicTag, assignId: true, builder: (_) { return _buildContent(); }, ); resultWidget = Scaffold( body: resultWidget, floatingActionButton: const NestedScrollviewTabBarViewDemoFloatingActionBtn(), ); return resultWidget; } Widget _buildContent() { Widget resultWidget = NestedScrollView( key: state.nestedScrollViewKey, controller: state.outerScrollController, headerSliverBuilder: (context, innerBoxIsScrolled) { return [ _buildSliverAppBar(innerBoxIsScrolled), const NestedScrollviewTabBarViewDemoHeaderListSliver(), SliverPersistentHeader( key: state.tabBarKey, pinned: true, delegate: SliverHeaderDelegate.fixedHeight( height: 50, child: const NestedScrollViewTabBarViewDemoTabBar(), ), ), ]; }, body: Builder(builder: (context) { final innerScrollController = PrimaryScrollController.of(context); if (nestedScrollUtil.bodyScrollController != innerScrollController) { nestedScrollUtil.bodyScrollController = innerScrollController; } return _buildTabBarView(); }), ); resultWidget = SliverViewObserver( child: resultWidget, sliverContexts: () { final sliverHeaderListCtx = state.headerSliverListCtx; final sliverTab1ListCtx = state.sliverTab1ListCtx; final sliverTab2GridCtx = state.tab2SliverGridCtx; final sliverTab3ListCtx = state.tab3SliverListCtx; final sliverTab3GridCtx = state.tab3SliverGridCtx; return [ if (sliverHeaderListCtx != null) sliverHeaderListCtx, if (sliverTab1ListCtx != null) sliverTab1ListCtx, if (sliverTab2GridCtx != null) sliverTab2GridCtx, if (sliverTab3ListCtx != null) sliverTab3ListCtx, if (sliverTab3GridCtx != null) sliverTab3GridCtx, ]; }, customOverlap: (sliverContext) { return nestedScrollUtil.calcOverlap( nestedScrollViewKey: state.nestedScrollViewKey, sliverContext: sliverContext, ); }, onObserveAll: logic.handleOnObserveAll, ); return resultWidget; } Widget _buildTabBarView() { return TabBarView( controller: state.tabController, children: const [ NestedScrollviewTabBarViewDemoTab1View(), NestedScrollviewTabBarViewDemoTab2View(), NestedScrollviewTabBarViewDemoTab3View(), ], ); } Widget _buildSliverAppBar(bool innerBoxIsScrolled) { return SliverAppBar( key: state.appBarKey, title: const Text("Nested & TabBarView"), pinned: true, forceElevated: innerBoxIsScrolled, actions: const [ NestedScrollviewTabBarViewDemoScrollTypeSwitch(), ], ); } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/state/nested_scrollview_tab_bar_view_demo_state.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-23 22:19:39 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; class NestedScrollViewTabBarViewDemoState { BuildContext? rootContext; bool scrollToWithAnimation = false; GlobalKey nestedScrollViewKey = GlobalKey(); GlobalKey appBarKey = GlobalKey(); GlobalKey tabBarKey = GlobalKey(); List tabTypeList = [ NestedScrollviewTabBarViewDemoTabType.tab1, NestedScrollviewTabBarViewDemoTabType.tab2, NestedScrollviewTabBarViewDemoTabType.tab3, ]; late TabController tabController; ScrollController outerScrollController = ScrollController(); ScrollController? bodyScrollController; late SliverObserverController observerController = SliverObserverController( controller: outerScrollController, ); late NestedScrollUtil nestedScrollUtil = NestedScrollUtil() ..outerScrollController = outerScrollController; BuildContext? headerSliverListCtx; int hitIndexForHeaderListCtx = 0; BuildContext? sliverTab1ListCtx; int hitIndexForTab1ListCtx = 0; BuildContext? tab2SliverGridCtx; List hitIndexesForTab2Grid = []; BuildContext? tab3SliverListCtx; int hitIndexForTab3ListCtx = 0; BuildContext? tab3SliverGridCtx; List hitIndexesForTab3Grid = []; } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_floating_action_btn.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 23:12:26 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_floating_action_btn.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/state/nested_scrollview_tab_bar_view_demo_state.dart'; class NestedScrollviewTabBarViewDemoFloatingActionBtn extends StatefulWidget { const NestedScrollviewTabBarViewDemoFloatingActionBtn({super.key}); @override State createState() => _NestedScrollviewTabBarViewDemoFloatingActionBtnState(); } class _NestedScrollviewTabBarViewDemoFloatingActionBtnState extends State with NestedScrollviewTabBarViewDemoLogicConsumerMixin< NestedScrollviewTabBarViewDemoFloatingActionBtn> { NestedScrollViewTabBarViewDemoState get state => logic.state; @override Widget build(BuildContext context) { return GetBuilder( tag: logicTag, id: NestedScrollviewTabBarViewDemoUpdateType.floatingActionButton, builder: (_) { return _buildBody(); }, ); } Widget _buildBody() { final index = state.tabController.index; final tabType = state.tabTypeList[index]; switch (tabType) { case NestedScrollviewTabBarViewDemoTabType.tab1: return Column( mainAxisSize: MainAxisSize.min, spacing: 10, children: [ FloatingActionButton( heroTag: 'header_list', onPressed: () { logic.handleFABClick( NestedScrollviewTabBarViewDemoFABClickType.headerSliverList, ); }, child: const Icon(Icons.list_alt_rounded), ), FloatingActionButton( heroTag: 'tab1_list', onPressed: () { logic.handleFABClick( NestedScrollviewTabBarViewDemoFABClickType.tab1SliverList, ); }, child: const Icon(Icons.list), ), ], ); case NestedScrollviewTabBarViewDemoTabType.tab2: return FloatingActionButton( heroTag: 'tab2_grid', onPressed: () { logic.handleFABClick( NestedScrollviewTabBarViewDemoFABClickType.tab2SliverGrid, ); }, child: const Icon(Icons.grid_view), ); case NestedScrollviewTabBarViewDemoTabType.tab3: return Column( mainAxisSize: MainAxisSize.min, spacing: 10, children: [ FloatingActionButton( heroTag: 'tab3_list', onPressed: () { logic.handleFABClick( NestedScrollviewTabBarViewDemoFABClickType.tab3SliverList, ); }, child: const Icon(Icons.list), ), FloatingActionButton( heroTag: 'tab3_grid', onPressed: () { logic.handleFABClick( NestedScrollviewTabBarViewDemoFABClickType.tab3SliverGrid, ); }, child: const Icon(Icons.grid_view), ), ], ); } } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_header_list_sliver.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 23:03:32 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_observer.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/state/nested_scrollview_tab_bar_view_demo_state.dart'; class NestedScrollviewTabBarViewDemoHeaderListSliver extends StatefulWidget { const NestedScrollviewTabBarViewDemoHeaderListSliver({super.key}); @override State createState() => _NestedScrollviewTabBarViewDemoHeaderListSliverState(); } class _NestedScrollviewTabBarViewDemoHeaderListSliverState extends State with NestedScrollviewTabBarViewDemoLogicConsumerMixin< NestedScrollviewTabBarViewDemoHeaderListSliver> { NestedScrollViewTabBarViewDemoState get state => logic.state; @override Widget build(BuildContext context) { return GetBuilder( tag: logicTag, id: NestedScrollviewTabBarViewDemoUpdateType.headerSliverList, builder: (_) { return _buildSliverList(); }, ); } Widget _buildSliverList() { return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { logic.updateNestedScrollUtilHeaderSliverContextsIfNeed( oldCtx: state.headerSliverListCtx, newCtx: ctx, toRecordCtx: () { state.headerSliverListCtx = ctx; }, ); return ListTile( title: Text("Header Item $index"), tileColor: state.hitIndexForHeaderListCtx == index ? Colors.orange : index % 2 == 0 ? Colors.grey[100] : Colors.white, ); }, childCount: 5, ), ); } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_scroll_type_switch.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 23:54:30 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_scroll_type_switch.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/state/nested_scrollview_tab_bar_view_demo_state.dart'; class NestedScrollviewTabBarViewDemoScrollTypeSwitch extends StatefulWidget { const NestedScrollviewTabBarViewDemoScrollTypeSwitch({super.key}); @override State createState() => _NestedScrollviewTabBarViewDemoScrollTypeSwitchState(); } class _NestedScrollviewTabBarViewDemoScrollTypeSwitchState extends State with NestedScrollviewTabBarViewDemoLogicConsumerMixin< NestedScrollviewTabBarViewDemoScrollTypeSwitch> { NestedScrollViewTabBarViewDemoState get state => logic.state; @override Widget build(BuildContext context) { return GetBuilder( tag: logicTag, id: NestedScrollviewTabBarViewDemoUpdateType.scrollTypeSwitch, builder: (_) { return _buildBody(); }, ); } Widget _buildBody() { return Switch( value: state.scrollToWithAnimation, onChanged: logic.handleScrollTypeSwitchOnChanged, ); } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_tab1_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 22:25:39 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_observer.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/state/nested_scrollview_tab_bar_view_demo_state.dart'; class NestedScrollviewTabBarViewDemoTab1View extends StatefulWidget { const NestedScrollviewTabBarViewDemoTab1View({super.key}); @override State createState() => _NestedScrollviewTabBarViewDemoTab1ViewState(); } class _NestedScrollviewTabBarViewDemoTab1ViewState extends State with NestedScrollviewTabBarViewDemoLogicConsumerMixin< NestedScrollviewTabBarViewDemoTab1View> { NestedScrollViewTabBarViewDemoState get state => logic.state; @override Widget build(BuildContext context) { return GetBuilder( tag: logicTag, id: NestedScrollviewTabBarViewDemoUpdateType.tab1View, builder: (_) { return _buildBody(); }, ); } Widget _buildBody() { return CustomScrollView( key: const PageStorageKey("tab1"), slivers: [ _buildSliverList(), ], ); } Widget _buildSliverList() { return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { logic.updateNestedScrollUtilBodySliverContextsIfNeed( oldCtx: state.sliverTab1ListCtx, newCtx: ctx, toRecordCtx: () { state.sliverTab1ListCtx = ctx; }, ); return ListTile( tileColor: state.hitIndexForTab1ListCtx == index ? Colors.red : null, title: Text("Tab 1 - List Item $index"), ); }, childCount: 30, ), ); } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_tab2_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 22:33:19 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_observer.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/state/nested_scrollview_tab_bar_view_demo_state.dart'; class NestedScrollviewTabBarViewDemoTab2View extends StatefulWidget { const NestedScrollviewTabBarViewDemoTab2View({super.key}); @override State createState() => _NestedScrollviewTabBarViewDemoTab2ViewState(); } class _NestedScrollviewTabBarViewDemoTab2ViewState extends State with NestedScrollviewTabBarViewDemoLogicConsumerMixin< NestedScrollviewTabBarViewDemoTab2View> { NestedScrollViewTabBarViewDemoState get state => logic.state; @override Widget build(BuildContext context) { return GetBuilder( tag: logicTag, id: NestedScrollviewTabBarViewDemoUpdateType.tab2View, builder: (_) { return _buildBody(); }, ); } Widget _buildBody() { return CustomScrollView( key: const PageStorageKey("tab2"), slivers: [ _buildSliverGrid(), ], ); } Widget _buildSliverGrid() { Widget resultWidget = SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 2.0, ), delegate: SliverChildBuilderDelegate( (ctx, index) { logic.updateNestedScrollUtilBodySliverContextsIfNeed( oldCtx: state.tab2SliverGridCtx, newCtx: ctx, toRecordCtx: () { state.tab2SliverGridCtx = ctx; }, ); return Container( color: state.hitIndexesForTab2Grid.contains(index) ? Colors.green : Colors.blue[100], alignment: Alignment.center, child: Text('Tab 2 - Grid Item $index'), ); }, childCount: 30, ), ); resultWidget = SliverPadding( padding: const EdgeInsets.all(8), sliver: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_tab3_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 22:35:11 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/logic/nested_scrollview_tab_bar_view_demo_logic_observer.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/state/nested_scrollview_tab_bar_view_demo_state.dart'; class NestedScrollviewTabBarViewDemoTab3View extends StatefulWidget { const NestedScrollviewTabBarViewDemoTab3View({super.key}); @override State createState() => _NestedScrollviewTabBarViewDemoTab3ViewState(); } class _NestedScrollviewTabBarViewDemoTab3ViewState extends State with NestedScrollviewTabBarViewDemoLogicConsumerMixin< NestedScrollviewTabBarViewDemoTab3View> { NestedScrollViewTabBarViewDemoState get state => logic.state; @override Widget build(BuildContext context) { return CustomScrollView( key: const PageStorageKey("tab3"), slivers: [ GetBuilder( tag: logicTag, id: NestedScrollviewTabBarViewDemoUpdateType.tab3ViewSliverList, builder: (_) => _buildSliverList(), ), GetBuilder( tag: logicTag, id: NestedScrollviewTabBarViewDemoUpdateType.tab3ViewSliverGrid, builder: (_) => _buildSliverGrid(), ), ], ); } Widget _buildSliverList() { return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { logic.updateNestedScrollUtilBodySliverContextsIfNeed( oldCtx: state.tab3SliverListCtx, newCtx: ctx, toRecordCtx: () { state.tab3SliverListCtx = ctx; }, ); return ListTile( tileColor: state.hitIndexForTab3ListCtx == index ? Colors.red : null, title: Text("Tab 3 - List Item $index"), ); }, childCount: 10, ), ); } Widget _buildSliverGrid() { Widget resultWidget = SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 2.0, ), delegate: SliverChildBuilderDelegate( (ctx, index) { logic.updateNestedScrollUtilBodySliverContextsIfNeed( oldCtx: state.tab3SliverGridCtx, newCtx: ctx, toRecordCtx: () { state.tab3SliverGridCtx = ctx; }, ); return Container( color: state.hitIndexesForTab3Grid.contains(index) ? Colors.green : Colors.green[100], alignment: Alignment.center, child: Text('Tab 3 - Grid Item $index'), ); }, childCount: 20, ), ); resultWidget = SliverPadding( padding: const EdgeInsets.all(8), sliver: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/widget/nested_scrollview_tab_bar_view_demo_tabbar.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2026-02-24 21:28:34 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/header/nested_scrollview_tab_bar_view_demo_header.dart'; import 'package:scrollview_observer_example/features/nested_scrollview/nested_scrollview_tab_bar_view_demo/state/nested_scrollview_tab_bar_view_demo_state.dart'; class NestedScrollViewTabBarViewDemoTabBar extends StatefulWidget { const NestedScrollViewTabBarViewDemoTabBar({super.key}); @override State createState() => _NestedScrollViewTabBarViewDemoTabBarState(); } class _NestedScrollViewTabBarViewDemoTabBarState extends State with NestedScrollviewTabBarViewDemoLogicConsumerMixin< NestedScrollViewTabBarViewDemoTabBar> { NestedScrollViewTabBarViewDemoState get state => logic.state; @override Widget build(BuildContext context) { Widget resultWidget = TabBar( controller: state.tabController, labelColor: Colors.blue, unselectedLabelColor: Colors.grey, indicatorSize: TabBarIndicatorSize.label, tabs: state.tabTypeList.map((e) => Tab(text: e.title)).toList(), ); return Container( color: Theme.of(context).scaffoldBackgroundColor, child: resultWidget, ); } } ================================================ FILE: example/lib/features/pageview/pageview_demo/pageview_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-08-03 14:09:53 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; class PageViewDemoPage extends StatefulWidget { const PageViewDemoPage({Key? key}) : super(key: key); @override State createState() => _PageViewDemoPageState(); } class _PageViewDemoPageState extends State { double offsetYDelta = 50; late PageController pageController; List pageItemBgPicList = [ '11898897', '26653530', '12974784', '943459', '4424178', '20433037', '4424137', '4955810', '4424137', '18847956', ] .map((id) => 'https://images.pexels.com/photos/$id/pexels-photo-$id.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2') .toList(); int get pageItemCount => pageItemBgPicList.length; List> pageItemOffsetYList = []; final observerController = ListObserverController(); @override void initState() { super.initState(); pageController = PageController( initialPage: 4, viewportFraction: 0.8, ); pageItemOffsetYList = List.generate( pageItemCount, (index) { return ValueNotifier(0); }, ); Future.delayed(const Duration(milliseconds: 100)).then((_) { observerController.dispatchOnceObserve(); }); } @override void dispose() { pageController.dispose(); for (var e in pageItemOffsetYList) { e.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( "PageView", style: TextStyle( color: Colors.white, ), ), backgroundColor: Colors.black87, leading: IconButton( icon: const Icon( Icons.arrow_back_ios_new, color: Colors.white, ), onPressed: () { Navigator.of(context).pop(); }, ), ), body: Stack( children: [ _buildMap(), Positioned( left: 0, right: 0, bottom: 0, child: _buildPageView(), ), ], ), ); } Widget _buildMap() { Widget resultWidget = SizedBox( width: 500, height: 900, child: Image.network( 'https://images.pexels.com/photos/41949/earth-earth-at-night-night-lights-41949.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', fit: BoxFit.fitHeight, ), ); resultWidget = Stack( children: [ resultWidget, Positioned( left: 0, right: 0, top: 0, bottom: 0, child: Container( color: Colors.black38, ), ), ], ); return resultWidget; } Widget _buildPageView() { Widget resultWidget = PageView.builder( controller: pageController, itemBuilder: (context, index) { return _buildPageItem(index); }, itemCount: pageItemCount, ); resultWidget = ListViewObserver( controller: observerController, child: resultWidget, triggerOnObserveType: ObserverTriggerOnObserveType.directly, onObserve: (resultModel) { final displayingChildModelList = resultModel.displayingChildModelList; for (var itemModel in displayingChildModelList) { final itemIndex = itemModel.index; final itemDisplayPercentage = itemModel.displayPercentage; // Calculates pageItemOffsetY final offsetY = (1 - itemDisplayPercentage) * offsetYDelta; pageItemOffsetYList[itemIndex].value = offsetY; } }, customTargetRenderSliverType: (renderObj) { return renderObj is RenderSliverFillViewport; }, ); resultWidget = SizedBox( height: 300, child: resultWidget, ); return resultWidget; } Widget _buildPageItem(int index) { Widget itemWidget = Container( decoration: BoxDecoration( color: Colors.blue[100], borderRadius: BorderRadius.circular(4), ), clipBehavior: Clip.antiAlias, alignment: Alignment.center, child: Stack( children: [ Positioned( left: 0, right: 0, top: 0, bottom: 0, child: _buildPageItemBgPicView(index), ), const SizedBox.expand(), Container( width: double.infinity, alignment: Alignment.center, height: 44, decoration: const BoxDecoration( color: Colors.white54, ), child: Text("Page $index"), ), ], ), ); Widget resultWidget = ValueListenableBuilder( valueListenable: pageItemOffsetYList[index], builder: (BuildContext context, double offsetY, Widget? child) { return Transform.translate( offset: Offset(0, offsetY), child: itemWidget, ); }, ); resultWidget = Container( margin: const EdgeInsets.symmetric(horizontal: 8), child: resultWidget, ); return resultWidget; } Widget _buildPageItemBgPicView(int index) { return Image.network( pageItemBgPicList[index], fit: BoxFit.cover, ); } } ================================================ FILE: example/lib/features/pageview/pageview_demo/pageview_parallax_item_listener_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-11-14 22:41:51 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; class PageViewParallaxItemListenerPage extends StatefulWidget { const PageViewParallaxItemListenerPage({Key? key}) : super(key: key); @override State createState() => _PageViewParallaxItemListenerPageState(); } class _PageViewParallaxItemListenerPageState extends State { late PageController pageController; List pageItemBgPicList = [ '11898897', '26653530', '12974784', '943459', '4424178', '20433037', '4424137', '4955810', '4424137', '18847956', ] .map((id) => 'https://images.pexels.com/photos/$id/pexels-photo-$id.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2') .toList(); int get pageItemCount => pageItemBgPicList.length; List> pageItemBgPicAlignmentXList = []; final observerController = ListObserverController(); @override void initState() { super.initState(); pageController = PageController( initialPage: 4, viewportFraction: 0.9, ); pageItemBgPicAlignmentXList = List.generate( pageItemCount, (index) { return ValueNotifier(0); }, ); Future.delayed(const Duration(milliseconds: 100)).then((_) { observerController.dispatchOnceObserve(); }); } @override void dispose() { pageController.dispose(); for (var e in pageItemBgPicAlignmentXList) { e.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.amber[50], appBar: AppBar( title: const Text( "PageView - Parallax", style: TextStyle( color: Colors.white, ), ), backgroundColor: Colors.black87, leading: IconButton( icon: const Icon( Icons.arrow_back_ios_new, color: Colors.white, ), onPressed: () { Navigator.of(context).pop(); }, ), ), body: Center( child: _buildPageView(), ), ); } Widget _buildPageView() { Widget resultWidget = PageView.builder( controller: pageController, itemBuilder: (context, index) { // return _buildPageItem(index); return ParallaxItemView( index: index, imgUrl: pageItemBgPicList[index], ); }, itemCount: pageItemCount, ); resultWidget = ListViewObserver( controller: observerController, child: resultWidget, triggerOnObserveType: ObserverTriggerOnObserveType.directly, customTargetRenderSliverType: (renderObj) { return renderObj is RenderSliverFillViewport; }, ); resultWidget = SizedBox( height: (MediaQuery.sizeOf(context).height - MediaQuery.paddingOf(context).top - kToolbarHeight) * 0.8, child: resultWidget, ); return resultWidget; } } class ParallaxItemView extends StatefulWidget { final int index; final String imgUrl; const ParallaxItemView({ Key? key, required this.index, required this.imgUrl, }) : super(key: key); @override State createState() => _ParallaxItemViewState(); } class _ParallaxItemViewState extends State { ListViewObserverState? observerState; final picAlignmentX = ValueNotifier(0); @override void didChangeDependencies() { super.didChangeDependencies(); removeListener(); observerState = ListViewObserver.of(context) ..addListener( onObserve: handleObserverResult, ); } @override void dispose() { removeListener(); picAlignmentX.dispose(); super.dispose(); } void removeListener() { observerState?.removeListener( onObserve: handleObserverResult, ); observerState = null; } void handleObserverResult( ListViewObserveModel result, ) { if (result.displayingChildModelMap.isEmpty) return; final model = result.displayingChildModelMap[widget.index]; if (model == null) { picAlignmentX.value = 0; return; } picAlignmentX.value = 1 - model.displayPercentage; if (model.leadingMarginToViewport > 0) { picAlignmentX.value = -picAlignmentX.value; } if (picAlignmentX.value > 1) { picAlignmentX.value = 1; } else if (picAlignmentX.value < -1) { picAlignmentX.value = -1; } } @override Widget build(BuildContext context) { Widget resultWidget = Stack( alignment: AlignmentDirectional.center, children: [ Positioned( left: 0, right: 0, top: 0, bottom: 0, child: _buildPageItemBgPicView(widget.index), ), const SizedBox.expand(), _buildNum(widget.index), ], ); resultWidget = Container( margin: const EdgeInsets.symmetric(horizontal: 8), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: Colors.blue[50], borderRadius: BorderRadius.circular(10), ), child: resultWidget, ); return resultWidget; } Widget _buildNum(int index) { return Container( alignment: Alignment.center, width: 80, height: 80, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(10), ), child: Text("Page $index"), ); } Widget _buildPageItemBgPicView(int index) { return ValueListenableBuilder( valueListenable: picAlignmentX, builder: (BuildContext context, double alignmentX, Widget? child) { return Image.network( widget.imgUrl, fit: BoxFit.cover, alignment: Alignment(alignmentX, 0), ); }, ); } } ================================================ FILE: example/lib/features/pageview/pageview_demo/pageview_parallax_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-08-26 21:30:59 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; class PageViewParallaxPage extends StatefulWidget { const PageViewParallaxPage({Key? key}) : super(key: key); @override State createState() => _PageViewParallaxPageState(); } class _PageViewParallaxPageState extends State { late PageController pageController; List pageItemBgPicList = [ '11898897', '26653530', '12974784', '943459', '4424178', '20433037', '4424137', '4955810', '4424137', '18847956', ] .map((id) => 'https://images.pexels.com/photos/$id/pexels-photo-$id.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2') .toList(); int get pageItemCount => pageItemBgPicList.length; List> pageItemBgPicAlignmentXList = []; final observerController = ListObserverController(); @override void initState() { super.initState(); pageController = PageController( initialPage: 4, viewportFraction: 0.9, ); pageItemBgPicAlignmentXList = List.generate( pageItemCount, (index) { return ValueNotifier(0); }, ); Future.delayed(const Duration(milliseconds: 100)).then((_) { observerController.dispatchOnceObserve(); }); } @override void dispose() { pageController.dispose(); for (var e in pageItemBgPicAlignmentXList) { e.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.amber[50], appBar: AppBar( title: const Text( "PageView - Parallax", style: TextStyle( color: Colors.white, ), ), backgroundColor: Colors.black87, leading: IconButton( icon: const Icon( Icons.arrow_back_ios_new, color: Colors.white, ), onPressed: () { Navigator.of(context).pop(); }, ), ), body: Center( child: _buildPageView(), ), ); } Widget _buildPageView() { Widget resultWidget = PageView.builder( controller: pageController, itemBuilder: (context, index) { return _buildPageItem(index); }, itemCount: pageItemCount, ); resultWidget = ListViewObserver( controller: observerController, child: resultWidget, triggerOnObserveType: ObserverTriggerOnObserveType.directly, onObserve: (resultModel) { final displayingChildModelList = resultModel.displayingChildModelList; for (var itemModel in displayingChildModelList) { final itemIndex = itemModel.index; final itemDisplayPercentage = itemModel.displayPercentage; // Calculates itemAlignmentX double itemAlignmentX = 1 - itemDisplayPercentage; if (itemModel.leadingMarginToViewport > 0) { itemAlignmentX = -itemAlignmentX; } if (itemAlignmentX > 1) { itemAlignmentX = 1; } else if (itemAlignmentX < -1) { itemAlignmentX = -1; } pageItemBgPicAlignmentXList[itemIndex].value = itemAlignmentX; } }, customTargetRenderSliverType: (renderObj) { return renderObj is RenderSliverFillViewport; }, ); resultWidget = SizedBox( height: (MediaQuery.sizeOf(context).height - MediaQuery.paddingOf(context).top - kToolbarHeight) * 0.8, child: resultWidget, ); return resultWidget; } Widget _buildPageItem(int index) { Widget resultWidget = Stack( alignment: AlignmentDirectional.center, children: [ Positioned( left: 0, right: 0, top: 0, bottom: 0, child: _buildPageItemBgPicView(index), ), const SizedBox.expand(), _buildNum(index), ], ); resultWidget = Container( margin: const EdgeInsets.symmetric(horizontal: 8), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: Colors.blue[50], borderRadius: BorderRadius.circular(10), ), child: resultWidget, ); return resultWidget; } Widget _buildNum(int index) { return Container( alignment: Alignment.center, width: 80, height: 80, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(10), ), child: Text("Page $index"), ); } Widget _buildPageItemBgPicView(int index) { return ValueListenableBuilder( valueListenable: pageItemBgPicAlignmentXList[index], builder: (BuildContext context, double alignmentX, Widget? child) { return Image.network( pageItemBgPicList[index], fit: BoxFit.cover, alignment: Alignment(alignmentX, 0), ); }, ); } } ================================================ FILE: example/lib/features/scene/anchor_demo/anchor_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; class AnchorListPage extends StatefulWidget { const AnchorListPage({Key? key}) : super(key: key); @override State createState() => _AnchorListPageState(); } class _AnchorListPageState extends State with SingleTickerProviderStateMixin { ScrollController scrollController = ScrollController(); late ListObserverController observerController; late TabController _tabController; List tabs = ["News(0)", "History(5)", "Picture(10)"]; List tabIndexes = [0, 5, 10]; @override void initState() { super.initState(); observerController = ListObserverController(controller: scrollController); _tabController = TabController(length: tabs.length, vsync: this); } @override void dispose() { observerController.controller?.dispose(); _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Anchor ListView"), bottom: PreferredSize( preferredSize: const Size(double.infinity, 44), child: TabBar( controller: _tabController, tabs: tabs.map((e) => Tab(text: e)).toList(), onTap: (index) { observerController.animateTo( index: tabIndexes[index], duration: const Duration(milliseconds: 250), curve: Curves.ease, ); }, ), ), ), body: ListViewObserver( controller: observerController, child: _buildListView(), onObserve: (resultModel) { _tabController.index = ObserverUtils.calcAnchorTabIndex( observeModel: resultModel, tabIndexes: tabIndexes, currentTabIndex: _tabController.index, ); }, ), ); } ListView _buildListView() { return ListView.separated( controller: scrollController, itemBuilder: (ctx, index) { return _buildListItemView(index); }, separatorBuilder: (ctx, index) { return _buildSeparatorView(); }, itemCount: 50, ); } Widget _buildListItemView(int index) { return Container( height: 300, color: Colors.black12, child: Center( child: Text( "index -- $index", style: const TextStyle( color: Colors.black, ), ), ), ); } Container _buildSeparatorView() { return Container( color: Colors.white, height: 5, ); } } ================================================ FILE: example/lib/features/scene/anchor_demo/anchor_waterfall_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-05-27 11:51:24 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:waterfall_flow/waterfall_flow.dart'; class AnchorWaterfallPage extends StatefulWidget { const AnchorWaterfallPage({Key? key}) : super(key: key); @override State createState() => _AnchorWaterfallPageState(); } class _AnchorWaterfallPageState extends State with SingleTickerProviderStateMixin { ScrollController scrollController = ScrollController(); late GridObserverController observerController; late TabController _tabController; List tabs = ["News(0)", "History(5)", "Picture(10)"]; List tabIndexes = [0, 5, 10]; @override void initState() { super.initState(); observerController = GridObserverController(controller: scrollController); _tabController = TabController(length: tabs.length, vsync: this); } @override void dispose() { observerController.controller?.dispose(); _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Anchor Waterfall"), bottom: PreferredSize( preferredSize: const Size(double.infinity, 44), child: TabBar( controller: _tabController, tabs: tabs.map((e) => Tab(text: e)).toList(), onTap: (index) { observerController.animateTo( index: tabIndexes[index], duration: const Duration(milliseconds: 250), curve: Curves.ease, ); }, ), ), ), body: GridViewObserver( child: _buildGridView(), controller: observerController, customTargetRenderSliverType: (renderObject) { return renderObject is RenderSliverWaterfallFlow; }, onObserve: (resultModel) { debugPrint( 'firstGroupChildIndexList -- ${resultModel.firstGroupChildList.map((e) => e.index).toList()}'); debugPrint( 'displayingChildIndexList -- ${resultModel.displayingChildIndexList}'); _tabController.index = ObserverUtils.calcAnchorTabIndex( observeModel: resultModel, tabIndexes: tabIndexes, currentTabIndex: _tabController.index, ); }, ), ); } Widget _buildGridView() { return WaterfallFlow.builder( controller: scrollController, gridDelegate: const SliverWaterfallFlowDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 15, crossAxisSpacing: 10, ), itemBuilder: (BuildContext context, int index) { return Container( alignment: Alignment.center, color: Colors.teal[100 * (index % 9)], child: Text('grid item $index'), height: 50.0 + 100.0 * (index % 9), ); }, itemCount: 20, ); } } ================================================ FILE: example/lib/features/scene/azlist_demo/azlist_cursor.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-10-28 15:56:01 */ import 'package:flutter/material.dart'; class AzListCursor extends StatelessWidget { final double size; final String title; final double arrowSize = 30; const AzListCursor({ Key? key, required this.size, required this.title, }) : super(key: key); @override Widget build(BuildContext context) { return Stack( clipBehavior: Clip.none, children: [ _buildTitle(), Positioned( right: -arrowSize * 0.5 - 2.5, top: (size - arrowSize) * 0.5, child: _buildArrow(), ), ], ); } Widget _buildArrow() { Widget resultWidget = Icon( Icons.arrow_right, color: Colors.black54, size: arrowSize, ); return resultWidget; } Widget _buildTitle() { Widget resultWidget = Text( title, style: const TextStyle(color: Colors.white, fontSize: 32), ); resultWidget = Container( width: size, height: size, alignment: Alignment.center, decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(5), ), child: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/azlist_demo/azlist_index_bar.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-10-28 11:35:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; class AzListIndexBar extends StatefulWidget { final GlobalKey parentKey; final List symbols; final void Function( int index, Offset cursorOffset, )? onSelectionUpdate; final void Function()? onSelectionEnd; const AzListIndexBar({ Key? key, required this.parentKey, required this.symbols, this.onSelectionUpdate, this.onSelectionEnd, }) : super(key: key); @override State createState() => _AzListIndexBarState(); } class _AzListIndexBarState extends State { ListObserverController observerController = ListObserverController(); double observeOffset = 0; ValueNotifier selectedIndex = ValueNotifier(-1); @override Widget build(BuildContext context) { Widget resultWidget = ListViewObserver( child: _buildListView(), controller: observerController, dynamicLeadingOffset: () => observeOffset, ); resultWidget = GestureDetector( onVerticalDragUpdate: _onGestureHandler, onVerticalDragDown: _onGestureHandler, onVerticalDragCancel: _onGestureEnd, onVerticalDragEnd: _onGestureEnd, child: resultWidget, ); return resultWidget; } Widget _buildListView() { return ValueListenableBuilder( valueListenable: selectedIndex, builder: (BuildContext context, int value, Widget? child) { return ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemBuilder: (context, index) { final isSelected = value == index; Widget resultWidget = Text( widget.symbols[index], style: TextStyle( fontSize: 11, color: isSelected ? Colors.white : Colors.black, ), ); resultWidget = Container( width: 18, height: 18, alignment: Alignment.center, decoration: BoxDecoration( color: isSelected ? Colors.blue : Colors.transparent, borderRadius: BorderRadius.circular(9), ), child: resultWidget, ); resultWidget = Align( child: resultWidget, alignment: Alignment.centerLeft, ); return resultWidget; }, itemCount: widget.symbols.length, ); }, ); } _onGestureHandler(dynamic details) async { if (details is! DragUpdateDetails && details is! DragDownDetails) return; observeOffset = details.localPosition.dy; final result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, ); final observeResult = result.observeResult; // Nothing has changed. if (observeResult == null) return; final firstChildModel = observeResult.firstChild; if (firstChildModel == null) return; final firstChildIndex = firstChildModel.index; selectedIndex.value = firstChildIndex; // Calculate cursor offset. final firstChildRenderObj = firstChildModel.renderObject; final firstChildRenderObjOffset = firstChildRenderObj.localToGlobal( Offset.zero, ancestor: widget.parentKey.currentContext?.findRenderObject(), ); final cursorOffset = Offset( firstChildRenderObjOffset.dx, firstChildRenderObjOffset.dy + firstChildModel.size.width * 0.5, ); widget.onSelectionUpdate?.call( firstChildIndex, cursorOffset, ); } _onGestureEnd([_]) { selectedIndex.value = -1; widget.onSelectionEnd?.call(); } } ================================================ FILE: example/lib/features/scene/azlist_demo/azlist_item_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-10-28 19:43:04 */ import 'package:flutter/material.dart'; class AzListItemView extends StatelessWidget { const AzListItemView({ Key? key, required this.name, this.isShowSeparator = true, }) : super(key: key); final String name; final bool isShowSeparator; @override Widget build(BuildContext context) { return Container( color: Colors.white, child: Container( height: 50, decoration: BoxDecoration( border: isShowSeparator ? Border( bottom: BorderSide( color: Colors.grey[300]!, width: 0.5, ), ) : null, ), alignment: Alignment.centerLeft, margin: const EdgeInsets.only(left: 16.0), child: Text( name, style: const TextStyle(color: Colors.black), ), ), ); } } ================================================ FILE: example/lib/features/scene/azlist_demo/azlist_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-10-28 10:19:23 */ import 'package:flutter/material.dart'; class AzListContactModel { final String section; final List names; AzListContactModel({ required this.section, required this.names, }); } class AzListCursorInfoModel { final String title; final Offset offset; AzListCursorInfoModel({ required this.title, required this.offset, }); } ================================================ FILE: example/lib/features/scene/azlist_demo/azlist_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-10-25 22:07:29 */ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/scene/azlist_demo/azlist_cursor.dart'; import 'package:scrollview_observer_example/features/scene/azlist_demo/azlist_item_view.dart'; import 'package:scrollview_observer_example/features/scene/azlist_demo/azlist_model.dart'; import 'package:scrollview_observer_example/features/scene/azlist_demo/azlist_index_bar.dart'; class AzListPage extends StatefulWidget { const AzListPage({Key? key}) : super(key: key); @override State createState() => _AzListPageState(); } class _AzListPageState extends State { List contactList = []; List get symbols => contactList.map((e) => e.section).toList(); final indexBarContainerKey = GlobalKey(); bool isShowListMode = true; ValueNotifier cursorInfo = ValueNotifier(null); double indexBarWidth = 20; ScrollController scrollController = ScrollController(); late SliverObserverController observerController; Map sliverContextMap = {}; generateContactData() { final a = const Utf8Codec().encode("A").first; final z = const Utf8Codec().encode("Z").first; int pointer = a; while (pointer >= a && pointer <= z) { final character = const Utf8Codec().decode(Uint8List.fromList([pointer])); contactList.add( AzListContactModel( section: character, names: List.generate(Random().nextInt(8), (index) { return '$character-$index'; }), ), ); pointer++; } } @override void initState() { super.initState(); observerController = SliverObserverController(controller: scrollController); generateContactData(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color.fromARGB(255, 243, 244, 246), appBar: AppBar( title: const Text("AzListPage"), backgroundColor: const Color.fromARGB(255, 243, 244, 246), shadowColor: Colors.transparent, foregroundColor: Colors.black, actions: [_buildSwitchModeBtn()], ), body: Stack( children: [ SliverViewObserver( controller: observerController, sliverContexts: () { return sliverContextMap.values.toList(); }, child: CustomScrollView( key: ValueKey(isShowListMode), controller: scrollController, slivers: contactList.mapIndexed((i, e) { return _buildSliver(index: i, model: e); }).toList(), ), ), _buildCursor(), Positioned( top: 0, bottom: 0, right: 0, child: _buildIndexBar(), ), ], ), ); } Widget _buildSwitchModeBtn() { return IconButton( onPressed: () { setState(() { isShowListMode = !isShowListMode; // Clear the offset cache. for (var ctx in sliverContextMap.values) { observerController.clearScrollIndexCache(sliverContext: ctx); } sliverContextMap.clear(); }); observerController.reattach(); }, icon: const Icon(Icons.swap_horizontal_circle_sharp), ); } Widget _buildCursor() { return ValueListenableBuilder( valueListenable: cursorInfo, builder: ( BuildContext context, AzListCursorInfoModel? value, Widget? child, ) { Widget resultWidget = Container(); double top = 0; double right = indexBarWidth + 8; if (value == null) { resultWidget = const SizedBox.shrink(); } else { double titleSize = 80; top = value.offset.dy - titleSize * 0.5; resultWidget = AzListCursor(size: titleSize, title: value.title); } resultWidget = Positioned( top: top, right: right, child: resultWidget, ); return resultWidget; }, ); } Widget _buildIndexBar() { return Container( key: indexBarContainerKey, width: indexBarWidth, alignment: Alignment.center, child: AzListIndexBar( parentKey: indexBarContainerKey, symbols: symbols, onSelectionUpdate: (index, cursorOffset) { cursorInfo.value = AzListCursorInfoModel( title: symbols[index], offset: cursorOffset, ); final sliverContext = sliverContextMap[index]; if (sliverContext == null) return; observerController.jumpTo( index: 0, sliverContext: sliverContext, ); }, onSelectionEnd: () { cursorInfo.value = null; }, ), ); } Widget _buildSliver({ required int index, required AzListContactModel model, }) { final names = model.names; if (names.isEmpty) return const SliverToBoxAdapter(); Widget resultWidget = isShowListMode ? SliverList( delegate: SliverChildBuilderDelegate( (context, itemIndex) { if (sliverContextMap[index] == null) { sliverContextMap[index] = context; } return AzListItemView(name: names[itemIndex]); }, childCount: names.length, ), ) : SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, //Grid按两列显示 mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 2.0, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int itemIndex) { if (sliverContextMap[index] == null) { sliverContextMap[index] = context; } return AzListItemView( name: names[itemIndex], isShowSeparator: false, ); }, childCount: names.length, ), ); resultWidget = SliverStickyHeader( header: Container( height: 44.0, color: const Color.fromARGB(255, 243, 244, 246), padding: const EdgeInsets.symmetric(horizontal: 16.0), alignment: Alignment.centerLeft, child: Text( model.section, style: const TextStyle(color: Colors.black54), ), ), sliver: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/chat_demo/helper/chat_data_helper.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-09-27 22:49:41 */ import 'dart:math'; import 'package:scrollview_observer_example/features/scene/chat_demo/model/chat_model.dart'; class ChatDataHelper { static List chatContents = [ 'My name is LinXunFeng', 'Twitter: https://twitter.com/xunfenghellolo' 'Github: https://github.com/LinXunFeng', 'Blog: https://fullstackaction.com/', 'Juejin: https://juejin.cn/user/1820446984512392/posts', 'Artile: Flutter-获取ListView当前正在显示的Widget信息\nhttps://juejin.cn/post/7103058155692621837', 'Artile: Flutter-列表滚动定位超强辅助库,墙裂推荐!🔥\nhttps://juejin.cn/post/7129888644290068487', 'A widget for observing data related to the child widgets being displayed in a scrollview.\nhttps://github.com/LinXunFeng/flutter_scrollview_observer', '📱 Swifty screen adaptation solution (Support Objective-C and Swift)\nhttps://github.com/LinXunFeng/SwiftyFitsize' ]; static ChatModel createChatModel({ bool? isOwn, }) { final random = Random(); final content = ChatDataHelper.chatContents[random.nextInt(chatContents.length)]; return ChatModel( isOwn: isOwn ?? random.nextBool(), content: content, ); } } ================================================ FILE: example/lib/features/scene/chat_demo/model/chat_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-09-25 21:41:13 */ class ChatModel { ChatModel({ required this.isOwn, required this.content, }); final bool isOwn; final String content; } ================================================ FILE: example/lib/features/scene/chat_demo/page/chat_gpt_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-06-04 15:12:22 */ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/scene/chat_demo/helper/chat_data_helper.dart'; import 'package:scrollview_observer_example/features/scene/chat_demo/model/chat_model.dart'; import 'package:scrollview_observer_example/features/scene/chat_demo/widget/chat_item_widget.dart'; class ChatGPTPage extends StatefulWidget { const ChatGPTPage({Key? key}) : super(key: key); @override State createState() => _ChatGPTPageState(); } class _ChatGPTPageState extends State { ScrollController scrollController = ScrollController(); late ListObserverController observerController; late ChatScrollObserver chatObserver; List chatModels = []; bool editViewReadOnly = false; TextEditingController editViewController = TextEditingController(); Timer? timer; @override void initState() { super.initState(); chatModels = createChatModels(num: 8); observerController = ListObserverController(controller: scrollController) ..cacheJumpIndexOffset = false; chatObserver = ChatScrollObserver(observerController) ..fixedPositionOffset = 5 ..toRebuildScrollViewCallback = () { setState(() {}); }; } @override void dispose() { stopMsgUpdateStream(); observerController.controller?.dispose(); editViewController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color.fromARGB(255, 7, 7, 7), appBar: AppBar( title: const Text("ChatGPT"), backgroundColor: const Color.fromARGB(255, 19, 19, 19), actions: [ IconButton( onPressed: () async { editViewController.text = ''; stopMsgUpdateStream(); _addMessage(isOwn: true); await Future.delayed(const Duration(seconds: 1)); insertGenerativeMsg(); }, icon: const Icon(Icons.add_comment), ) ], ), body: _buildBody(), ); } Widget _buildBody() { Widget resultWidget = Column( children: [ Expanded(child: _buildListView()), _buildEditView(), const SafeArea(top: false, child: SizedBox.shrink()), ], ); return resultWidget; } Widget _buildEditView() { return Container( margin: const EdgeInsets.symmetric(horizontal: 25, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 0.5), borderRadius: BorderRadius.circular(4), // color: Colors.white, ), child: Row( children: [ Expanded( child: TextField( decoration: const InputDecoration( border: InputBorder.none, isCollapsed: true, ), style: const TextStyle(color: Colors.white), maxLines: 4, minLines: 1, showCursor: true, readOnly: editViewReadOnly, controller: editViewController, ), ), IconButton( icon: const Icon(Icons.emoji_emotions_outlined), iconSize: 24, color: Colors.white, padding: EdgeInsets.zero, constraints: const BoxConstraints.tightForFinite(), onPressed: () { setState(() { editViewReadOnly = !editViewReadOnly; }); }, ), ], ), ); } Widget _buildListView() { Widget resultWidget = ListView.builder( physics: ChatObserverClampingScrollPhysics(observer: chatObserver), padding: const EdgeInsets.only(left: 10, right: 10, top: 15, bottom: 15), shrinkWrap: chatObserver.isShrinkWrap, reverse: true, controller: scrollController, itemBuilder: ((context, index) { return ChatItemWidget( chatModel: chatModels[index], index: index, itemCount: chatModels.length, onRemove: () { chatObserver.standby(isRemove: true); setState(() { chatModels.removeAt(index); }); }, ); }), itemCount: chatModels.length, ); resultWidget = ListViewObserver( controller: observerController, child: resultWidget, ); resultWidget = Align( child: resultWidget, alignment: Alignment.topCenter, ); return resultWidget; } List createChatModels({int num = 3}) { return Iterable.generate(num) .map((e) => ChatDataHelper.createChatModel()) .toList(); } _addMessage({ required bool isOwn, }) { chatObserver.standby(changeCount: 1); setState(() { chatModels.insert(0, ChatDataHelper.createChatModel(isOwn: isOwn)); }); } insertGenerativeMsg() { stopMsgUpdateStream(); _addMessage(isOwn: false); int count = 0; timer = Timer.periodic(const Duration(milliseconds: 100), (timer) { if (count >= 60) { stopMsgUpdateStream(); return; } count++; final model = chatModels.first; final newString = '${model.content}-1+1'; final newModel = ChatModel(isOwn: model.isOwn, content: newString); chatModels[0] = newModel; chatObserver.standby( mode: ChatScrollObserverHandleMode.generative, // changeCount: 1, ); // chatObserver.standby( // mode: ChatScrollObserverHandleMode.specified, // refIndexType: // ChatScrollObserverRefIndexType.relativeIndexStartFromCacheExtent, // refItemIndex: 1, // refItemIndexAfterUpdate: 1, // ); // chatObserver.standby( // mode: ChatScrollObserverHandleMode.specified, // refIndexType: // ChatScrollObserverRefIndexType.relativeIndexStartFromDisplaying, // refItemIndex: 1, // refItemIndexAfterUpdate: 1, // ); // chatObserver.standby( // mode: ChatScrollObserverHandleMode.specified, // refIndexType: ChatScrollObserverRefIndexType.itemIndex, // refItemIndex: 1, // refItemIndexAfterUpdate: 1, // ); setState(() {}); }); } stopMsgUpdateStream() { timer?.cancel(); timer = null; } } ================================================ FILE: example/lib/features/scene/chat_demo/page/chat_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-29 23:43:08 */ import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/scene/chat_demo/helper/chat_data_helper.dart'; import 'package:scrollview_observer_example/features/scene/chat_demo/model/chat_model.dart'; import 'package:scrollview_observer_example/features/scene/chat_demo/widget/chat_item_widget.dart'; import 'package:scrollview_observer_example/features/scene/chat_demo/widget/chat_unread_tip_view.dart'; import 'package:scrollview_observer_example/utils/keyboard.dart'; import 'package:scrollview_observer_example/utils/random.dart'; class ChatPage extends StatefulWidget { const ChatPage({Key? key}) : super(key: key); @override State createState() => _ChatPageState(); } class _ChatPageState extends State with WidgetsBindingObserver { ScrollController scrollController = ScrollController(); late ListObserverController observerController; late ChatScrollObserver chatObserver; List chatModels = []; ValueNotifier unreadMsgCount = ValueNotifier(0); bool needIncrementUnreadMsgCount = false; bool editViewReadOnly = false; TextEditingController editViewController = TextEditingController(); BuildContext? pageOverlayContext; final LayerLink layerLink = LayerLink(); bool isShowClassicHeaderAndFooter = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); chatModels = createChatModels(); scrollController.addListener(scrollControllerListener); observerController = ListObserverController(controller: scrollController) ..cacheJumpIndexOffset = false; chatObserver = ChatScrollObserver(observerController) ..fixedPositionOffset = 5 ..toRebuildScrollViewCallback = () { setState(() {}); } ..onHandlePositionResultCallback = (result) { if (!needIncrementUnreadMsgCount) return; switch (result.type) { case ChatScrollObserverHandlePositionType.keepPosition: updateUnreadMsgCount(changeCount: result.changeCount); break; case ChatScrollObserverHandlePositionType.none: updateUnreadMsgCount(isReset: true); break; } }; Future.delayed(const Duration(seconds: 1), addUnreadTipView); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); observerController.controller?.dispose(); editViewController.dispose(); super.dispose(); } @override void didChangeMetrics() { super.didChangeMetrics(); // Update shrinkWrap in real time as the keyboard pops up or closes. chatObserver.observeSwitchShrinkWrap(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { if (MediaQuery.of(context).viewInsets.bottom == 0) { // Keyboard closes } else { // Keyboard pops up scrollController.jumpTo(0); } } }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color.fromARGB(255, 100, 100, 100), appBar: AppBar( title: const Text("Chat"), backgroundColor: const Color.fromARGB(255, 19, 19, 19), actions: [ TextButton( onPressed: () { isShowClassicHeaderAndFooter = !isShowClassicHeaderAndFooter; setState(() {}); }, child: Text( isShowClassicHeaderAndFooter ? "Classic" : "Material", style: const TextStyle( fontSize: 18, ), ), ), IconButton( onPressed: () { editViewController.text = ''; _addMessage(RandomTool.genInt(min: 1, max: 3)); }, icon: const Icon(Icons.add_comment), ) ], ), body: _buildBody(), ); } Widget _buildPageOverlay() { return Overlay(initialEntries: [ OverlayEntry( builder: (context) { pageOverlayContext = context; return Container(); }, ) ]); } Widget _buildBody() { Widget resultWidget = _buildListView(); // Dismiss keyboard when clicking or dragging ScrollView. resultWidget = GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { KeyboardTool.dismissKeyboard(context); }, onPanDown: (_) { KeyboardTool.dismissKeyboard(context); }, child: resultWidget, ); resultWidget = Column( children: [ Expanded(child: resultWidget), CompositedTransformTarget( link: layerLink, child: Container(), ), _buildEditView(), const SafeArea(top: false, child: SizedBox.shrink()), ], ); resultWidget = Stack(children: [ resultWidget, _buildPageOverlay(), ]); return resultWidget; } Widget _buildEditView() { return Container( margin: const EdgeInsets.symmetric(horizontal: 25, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 0.5), borderRadius: BorderRadius.circular(4), // color: Colors.white, ), child: Row( children: [ Expanded( child: TextField( decoration: const InputDecoration( border: InputBorder.none, isCollapsed: true, ), style: const TextStyle(color: Colors.white), maxLines: 4, minLines: 1, showCursor: true, readOnly: editViewReadOnly, controller: editViewController, ), ), IconButton( icon: const Icon(Icons.emoji_emotions_outlined), iconSize: 24, color: Colors.white, padding: EdgeInsets.zero, constraints: const BoxConstraints.tightForFinite(), onPressed: () { setState(() { editViewReadOnly = !editViewReadOnly; }); }, ), ], ), ); } Widget _buildUnreadTipView() { return ValueListenableBuilder( builder: (context, value, child) { return ChatUnreadTipView( unreadMsgCount: unreadMsgCount.value, onTap: () { scrollController.animateTo( 0, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); updateUnreadMsgCount(isReset: true); }, ); }, valueListenable: unreadMsgCount, ); } Widget _buildListView() { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { Widget resultWidget = EasyRefresh.builder( header: isShowClassicHeaderAndFooter ? const ClassicHeader() : const MaterialHeader(), footer: isShowClassicHeaderAndFooter ? const ClassicFooter( position: IndicatorPosition.above, infiniteOffset: null, ) : const MaterialFooter(), onRefresh: () async { await Future.delayed(const Duration(seconds: 2)); }, onLoad: () async { await Future.delayed(const Duration(seconds: 2)); }, childBuilder: (context, physics) { var scrollViewPhysics = physics.applyTo(ChatObserverClampingScrollPhysics( observer: chatObserver, )); Widget resultWidget = ListView.builder( physics: chatObserver.isShrinkWrap ? const NeverScrollableScrollPhysics() : scrollViewPhysics, padding: const EdgeInsets.only( left: 10, right: 10, top: 15, bottom: 15, ), shrinkWrap: chatObserver.isShrinkWrap, reverse: true, controller: scrollController, itemBuilder: ((context, index) { return ChatItemWidget( chatModel: chatModels[index], index: index, itemCount: chatModels.length, onRemove: () { chatObserver.standby(isRemove: true); setState(() { chatModels.removeAt(index); }); }, ); }), itemCount: chatModels.length, ); if (chatObserver.isShrinkWrap) { resultWidget = SingleChildScrollView( reverse: true, physics: scrollViewPhysics, child: Container( alignment: Alignment.topCenter, child: resultWidget, height: constraints.maxHeight + 0.001, ), ); } return resultWidget; }, ); resultWidget = ListViewObserver( controller: observerController, child: resultWidget, ); resultWidget = Align( child: resultWidget, alignment: Alignment.topCenter, ); return resultWidget; }, ); } addUnreadTipView() { Overlay.of(pageOverlayContext!).insert(OverlayEntry( builder: (BuildContext context) => UnconstrainedBox( child: CompositedTransformFollower( link: layerLink, followerAnchor: Alignment.bottomRight, targetAnchor: Alignment.topRight, offset: const Offset(-20, 0), child: Material( type: MaterialType.transparency, // color: Colors.green, child: _buildUnreadTipView(), ), ), ), )); } List createChatModels({int num = 3}) { return Iterable.generate(num) .map((e) => ChatDataHelper.createChatModel()) .toList(); } _addMessage(int count) { chatObserver.standby(changeCount: count); setState(() { needIncrementUnreadMsgCount = true; for (var i = 0; i < count; i++) { chatModels.insert(0, ChatDataHelper.createChatModel()); } }); } updateUnreadMsgCount({ bool isReset = false, int changeCount = 1, }) { needIncrementUnreadMsgCount = false; if (isReset) { unreadMsgCount.value = 0; } else { unreadMsgCount.value += changeCount; } } scrollControllerListener() { if (scrollController.offset < 50) { updateUnreadMsgCount(isReset: true); } } } ================================================ FILE: example/lib/features/scene/chat_demo/widget/chat_item_widget.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-09-27 22:46:36 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/chat_demo/model/chat_model.dart'; class ChatItemWidget extends StatelessWidget { const ChatItemWidget({ Key? key, required this.chatModel, required this.index, required this.itemCount, this.onRemove, }) : super(key: key); final ChatModel chatModel; final int index; final int itemCount; final Function? onRemove; @override Widget build(BuildContext context) { final isOwn = chatModel.isOwn; final nickName = isOwn ? 'LXF' : 'LQR'; Widget resultWidget = Row( textDirection: isOwn ? TextDirection.ltr : TextDirection.rtl, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: isOwn ? Colors.blue : Colors.white30, ), child: Center( child: Text( nickName, style: const TextStyle( color: Colors.white, ), ), ), ), const SizedBox(width: 10), Expanded( child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: isOwn ? const Color.fromARGB(255, 21, 125, 200) : const Color.fromARGB(255, 39, 39, 38), borderRadius: BorderRadius.circular(4), ), child: Text( '------------ ${itemCount - index} ------------ \n ${chatModel.content}', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ), const SizedBox(width: 50), ], ); resultWidget = Column( children: [ resultWidget, const SizedBox(height: 15), ], ); resultWidget = Dismissible( key: UniqueKey(), child: resultWidget, onDismissed: (_) { onRemove?.call(); }, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/chat_demo/widget/chat_unread_tip_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-10-31 15:40:04 */ import 'package:flutter/material.dart'; class ChatUnreadTipView extends StatelessWidget { ChatUnreadTipView({ Key? key, required this.unreadMsgCount, this.onTap, }) : super(key: key); final int unreadMsgCount; final Color primaryColor = Colors.green[100]!; final GestureTapCallback? onTap; @override Widget build(BuildContext context) { if (unreadMsgCount == 0) return const SizedBox.shrink(); Widget resultWidget = Stack( children: [ const Icon( Icons.mode_comment, size: 50, color: Colors.white, ), Container( margin: const EdgeInsets.only(top: 12), width: 50, child: Center( child: Text( '$unreadMsgCount', style: const TextStyle( color: Colors.blue, fontSize: 17, fontWeight: FontWeight.bold, ), ), ), ), ], ); resultWidget = GestureDetector( child: resultWidget, onTap: onTap, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/detail/header/detail_header.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 19:57:05 */ import 'package:flutter/material.dart'; import 'package:getx_helper/getx_helper.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic.dart'; typedef DetailLogicPutMixin = GetxLogicPutStateMixin; typedef DetailLogicConsumerMixin = GetxLogicConsumerStateMixin; enum DetailUpdateType { navBar, config, loading, module3, module6, } enum DetailModuleType { module1, module2, module3, module4, module5, module6, module7, module8, } enum DetailRefreshIndicatorType { none, footer, } ================================================ FILE: example/lib/features/scene/detail/logic/detail_logic.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 19:57:05 */ import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic_list_view.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic_nav_bar.dart'; import 'package:scrollview_observer_example/features/scene/detail/state/detail_state.dart'; class DetailLogic extends GetxController with GetTickerProviderStateMixin { final DetailState state = DetailState(); @override void onInit() { super.onInit(); onInitForNavBar(); onInitForListView(); } void onDispose() { state.isDisposed = true; onDisposeForNavBar(); onDisposeForListView(); } } ================================================ FILE: example/lib/features/scene/detail/logic/detail_logic_config.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-05 22:38:33 */ import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic_list_view.dart'; extension DetailLogicForConfig on DetailLogic { void onConfigConfirm() { state.defaultModuleAnchor = state.configSelectedAnchor; state.showConfig = false; initIndexPositionForListView(); // The synchronization data required for the ListView is loaded. update(); // Now it's the first time the ListView is rendered. firstTimeRenderListView(); // Load asynchronous data. loadAsyncDataForListView(); } } ================================================ FILE: example/lib/features/scene/detail/logic/detail_logic_list_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-03 21:02:30 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/common/route/route.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic_nav_bar.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; extension DetailLogicForListView on DetailLogic { void onInitForListView() { state.scrollController.addListener(() { if (!state.scrollController.hasClients) return; updateNavBarAlpha(); }); } void onDisposeForListView() { state.scrollController.dispose(); } void onObserveForListView(ListViewObserveModel result) { final navBarTabController = state.navBarTabController; if (navBarTabController == null) return; final index = ObserverUtils.calcAnchorTabIndex( observeModel: result, tabIndexes: state.navBarTabs.map((e) => e.index).toList(), currentTabIndex: navBarTabController.index, ); updateNavBarTabIndex(index); } void initIndexPositionForListView() { final defaultIndexModel = ObserverIndexPositionModel( index: 0, ); ObserverIndexPositionModel indexModel = defaultIndexModel; () { final moduleAnchor = state.defaultModuleAnchor; if (moduleAnchor == null) return; // Modules loaded asynchronously do not need to be processed. if (state.asyncLoadModuleTypes.contains(moduleAnchor)) return; indexModel = ObserverIndexPositionModel( index: state.moduleTypes.indexOf(moduleAnchor), offset: (_) => state.navBarHeight, ); }(); state.observerController.initialIndexModel = indexModel; } /// Specifically designed to handle those module anchors that are loaded /// asynchronously. void checkAnchorForListView(DetailModuleType moduleType) async { final moduleAnchor = state.defaultModuleAnchor; if (moduleAnchor == null) return; if (moduleType != moduleAnchor) return; if (!state.moduleTypes.contains(moduleType)) return; final index = state.moduleTypes.indexOf(moduleType); await WidgetsBinding.instance.endOfFrame; state.observerController.animateTo( index: index, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, offset: (_) => state.navBarHeight, ); } /// When updating the module widget, keep the current position unchanged. void updateAndKeepPositionForListView([ List? ids, ]) { () async { final result = await state.observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); final observeResult = result.observeResult; if (observeResult == null) return; final firstChild = observeResult.firstChild; if (firstChild == null) return; final refItemIndex = firstChild.index; // Keep position state.keepPositionObserver.standby( mode: ChatScrollObserverHandleMode.specified, refItemIndex: refItemIndex, refItemIndexAfterUpdate: refItemIndex, ); }(); update(ids); } /// Show loading first, and hide loading after setting the index of the /// TabBar and scrolling to the corresponding module. void firstTimeRenderListView() async { await Future.delayed(const Duration(milliseconds: 100)); state.showLoading = false; update([DetailUpdateType.loading]); } void loadAsyncDataForListView() { // Load Module3 and Module6 asynchronously Future.delayed(const Duration(seconds: 2)).then((_) { if (state.isDisposed) return; state.haveDataForModule3 = true; // update([DetailUpdateType.module3]); updateAndKeepPositionForListView([DetailUpdateType.module3]); checkAnchorForListView(DetailModuleType.module3); SnackBarUtil.showSnackBar( context: NavigationService.context, text: 'Module3 has been displayed', ); }); Future.delayed(const Duration(seconds: 3)).then((_) { if (state.isDisposed) return; state.haveDataForModule6 = true; // update([DetailUpdateType.module6]); updateAndKeepPositionForListView([DetailUpdateType.module6]); checkAnchorForListView(DetailModuleType.module6); SnackBarUtil.showSnackBar( context: NavigationService.context, text: 'Module6 has been displayed', ); }); } } ================================================ FILE: example/lib/features/scene/detail/logic/detail_logic_nav_bar.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-03 21:06:31 */ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic.dart'; import 'package:scrollview_observer_example/features/scene/detail/model/detail_nav_bar_tab_model.dart'; extension DetailLogicForNavBar on DetailLogic { void onInitForNavBar() { state.navBarTabs = [ createNavBarTabModel(DetailModuleType.module1), createNavBarTabModel(DetailModuleType.module4), createNavBarTabModel(DetailModuleType.module7), ]; state.navBarTabController = TabController( length: state.navBarTabs.length, vsync: this, ); } DetailNavBarTabModel createNavBarTabModel( DetailModuleType type, ) { return DetailNavBarTabModel( type: type, index: state.moduleTypes.indexOf(type), ); } void onDisposeForNavBar() { state.navBarTabController?.dispose(); state.navBarTabController = null; } void handleNavBarTabTap(int index) { if (!state.scrollController.hasClients) return; final tabModel = state.navBarTabs[index]; final moduleIndex = tabModel.index; if (moduleIndex == 0) { state.scrollController.jumpTo(0); return; } state.observerController.jumpTo( index: moduleIndex, offset: (_) => state.navBarHeight, ); } void updateNavBarTabIndex(int index) { final navBarTabController = state.navBarTabController; if (navBarTabController == null) return; navBarTabController.index = index; } void updateNavBarAlpha() { if (!state.scrollController.position.hasPixels) return; state.scrollController.position.pixels; state.navBarHeight; state.navBarAlpha; final scrollOffset = state.scrollController.position.pixels; final navBarHeight = state.navBarHeight; double newAlpha = 0.0; newAlpha = min( 1.0, max(0.0, scrollOffset / navBarHeight), ); if (state.navBarAlpha == newAlpha) return; state.navBarAlpha = newAlpha; update([ DetailUpdateType.navBar, ]); } } ================================================ FILE: example/lib/features/scene/detail/model/detail_nav_bar_tab_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-04 22:18:57 */ import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; class DetailNavBarTabModel { final DetailModuleType type; final int index; String get title => '${type.name[0].toUpperCase()}${type.name.substring(1)}'; DetailNavBarTabModel({ required this.type, required this.index, }); } ================================================ FILE: example/lib/features/scene/detail/page/detail_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 19:57:05 */ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic.dart'; import 'package:scrollview_observer_example/features/scene/detail/state/detail_state.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_config_view.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_list_view.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_nav_bar.dart'; class DetailPage extends StatefulWidget { const DetailPage({super.key}); @override State createState() => DetailPageState(); } class DetailPageState extends State with DetailLogicPutMixin { DetailState get state => logic.state; @override void dispose() { logic.onDispose(); super.dispose(); } @override DetailLogic initLogic() => DetailLogic(); @override Widget buildBody(BuildContext context) { return GetBuilder( tag: logicTag, assignId: true, builder: (_) { return Scaffold( appBar: _buildAppBar(), body: _buildBody(), ); }, ); } AppBar _buildAppBar() { return AppBar( title: const Text('Detail Page'), ); } Widget _buildBody() { if (state.showConfig) { return const DetailConfigView(); } return Stack( children: [ const DetailListView(), const Positioned( top: 0, left: 0, right: 0, child: DetailNavBar(), ), _buildLoading(), ], ); } Widget _buildLoading() { return GetBuilder( tag: logicTag, id: DetailUpdateType.loading, builder: (_) { if (!state.showLoading) return const SizedBox.shrink(); return Positioned.fill( child: Container( color: Colors.white, child: const CupertinoActivityIndicator(), ), ); }, ); } } ================================================ FILE: example/lib/features/scene/detail/state/detail_state.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 19:57:05 */ import 'package:scrollview_observer_example/features/scene/detail/state/detail_state_config.dart'; import 'package:scrollview_observer_example/features/scene/detail/state/detail_state_list_view.dart'; import 'package:scrollview_observer_example/features/scene/detail/state/detail_state_nav_bar.dart'; class DetailState with DetailStateForNavBar, DetailStateForListView, DetailStateForConfig { bool isDisposed = false; bool showLoading = true; } ================================================ FILE: example/lib/features/scene/detail/state/detail_state_config.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-05 22:42:42 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; mixin DetailStateForConfig { bool showConfig = true; DetailModuleType configSelectedAnchor = DetailModuleType.module7; List> get configDefaultAnchorEntries { List> entries = []; for (final moduleType in DetailModuleType.values) { entries.add( DropdownMenuEntry( value: moduleType, label: moduleType.name, ), ); } return entries; } DetailRefreshIndicatorType configRefreshIndicator = DetailRefreshIndicatorType.none; List> get configRefreshIndicatorEntries { List> entries = []; for (final moduleType in DetailRefreshIndicatorType.values) { entries.add( DropdownMenuEntry( value: moduleType, label: moduleType.name, ), ); } return entries; } } ================================================ FILE: example/lib/features/scene/detail/state/detail_state_list_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-03 22:01:50 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; mixin DetailStateForListView { DetailModuleType? defaultModuleAnchor; List moduleTypes = DetailModuleType.values.toList(); ScrollController scrollController = ScrollController(); late ListObserverController observerController = ListObserverController( controller: scrollController, ) ..observeIntervalForScrolling = const Duration(milliseconds: 50) // Since there are modules loaded asynchronously, which will cause the // cache to be offset inaccurately, so it is set to false here ..cacheJumpIndexOffset = false; late ChatScrollObserver keepPositionObserver = ChatScrollObserver( observerController, )..fixedPositionOffset = -1; List asyncLoadModuleTypes = [ DetailModuleType.module3, DetailModuleType.module6, ]; bool haveDataForModule3 = false; bool haveDataForModule6 = false; } ================================================ FILE: example/lib/features/scene/detail/state/detail_state_nav_bar.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-03 21:08:34 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/detail/model/detail_nav_bar_tab_model.dart'; mixin DetailStateForNavBar { final double navBarHeight = kToolbarHeight; double navBarAlpha = 0; TabController? navBarTabController; List navBarTabs = []; } ================================================ FILE: example/lib/features/scene/detail/widget/detail_config_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-10 12:50:51 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic_config.dart'; import 'package:scrollview_observer_example/features/scene/detail/state/detail_state.dart'; class DetailConfigView extends StatefulWidget { const DetailConfigView({super.key}); @override State createState() => _DetailConfigViewState(); } class _DetailConfigViewState extends State with DetailLogicConsumerMixin { DetailState get state => logic.state; @override Widget build(BuildContext context) { return GetBuilder( tag: logicTag, id: DetailUpdateType.config, builder: (_) { Widget resultWidget = Column( children: [ Expanded( child: _buildListView(), ), _buildConfirmBtn(), ], ); resultWidget = SafeArea( top: false, child: resultWidget, ); return resultWidget; }, ); } Widget _buildListView() { return ListView( children: [ _buildDefaultAnchor(), _buildRefreshPosition(), ], ); } Widget _buildDefaultAnchor() { return ListTile( title: const Text('Default Anchor'), trailing: DropdownMenu( initialSelection: state.configSelectedAnchor, dropdownMenuEntries: state.configDefaultAnchorEntries, onSelected: (DetailModuleType? module) { if (module == null) return; state.configSelectedAnchor = module; }, inputDecorationTheme: inputDecorationTheme(), ), ); } Widget _buildRefreshPosition() { return ListTile( title: const Text('Refresh Indicator'), trailing: DropdownMenu( initialSelection: state.configRefreshIndicator, dropdownMenuEntries: state.configRefreshIndicatorEntries, onSelected: (DetailRefreshIndicatorType? position) { if (position == null) return; state.configRefreshIndicator = position; }, inputDecorationTheme: inputDecorationTheme(), ), ); } Widget _buildConfirmBtn() { Widget resultWidget = Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16), child: ElevatedButton( onPressed: () { logic.onConfigConfirm(); }, child: const Text('Confirm'), ), ); resultWidget = Container( margin: const EdgeInsets.only(bottom: 15), child: resultWidget, ); return resultWidget; } InputDecorationTheme inputDecorationTheme() { return const InputDecorationTheme( isCollapsed: true, ); } } ================================================ FILE: example/lib/features/scene/detail/widget/detail_list_item_wrapper.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-03 15:07:52 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; class DetailListItemWrapper extends StatefulWidget { final String? title; final Widget child; const DetailListItemWrapper({ super.key, required this.child, this.title, }); @override State createState() => _DetailListItemWrapperState(); } class _DetailListItemWrapperState extends State with DetailLogicConsumerMixin { String get title => widget.title ?? ''; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTitle(), widget.child, const SizedBox(height: 10), ], ); } Widget _buildTitle() { if (title.isEmpty) return const SizedBox.shrink(); Widget resultWidget = Text( title, style: Theme.of(context).textTheme.titleLarge, ); resultWidget = Padding( padding: const EdgeInsets.only( top: 16, left: 8, bottom: 8, ), child: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/detail/widget/detail_list_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 20:03:56 */ import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic_list_view.dart'; import 'package:scrollview_observer_example/features/scene/detail/state/detail_state.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/list_item/detail_list_module1.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/list_item/detail_list_module2.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/list_item/detail_list_module3.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/list_item/detail_list_module4.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/list_item/detail_list_module5.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/list_item/detail_list_module6.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/list_item/detail_list_module7.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/list_item/detail_list_module8.dart'; class DetailListView extends StatefulWidget { const DetailListView({super.key}); @override State createState() => _DetailListViewState(); } class _DetailListViewState extends State with DetailLogicConsumerMixin { DetailState get state => logic.state; List get moduleTypes => state.moduleTypes; @override Widget build(BuildContext context) { Widget resultWidget; switch (state.configRefreshIndicator) { case DetailRefreshIndicatorType.none: resultWidget = _buildListView(); break; case DetailRefreshIndicatorType.footer: resultWidget = EasyRefresh.builder( footer: const MaterialFooter(), onLoad: () async { await Future.delayed(const Duration(seconds: 2)); }, childBuilder: (context, physics) { return _buildListView(physics: physics); }, ); break; } return resultWidget; } Widget _buildListView({ ScrollPhysics? physics, }) { ScrollPhysics _physics = ChatObserverClampingScrollPhysics( observer: state.keepPositionObserver, ); if (physics != null) { _physics = physics.applyTo(_physics); } Widget resultWidget = ListView.separated( controller: state.scrollController, physics: _physics, itemBuilder: (context, index) { switch (moduleTypes[index]) { case DetailModuleType.module1: return const DetailListModule1(); case DetailModuleType.module2: return const DetailListModule2(); case DetailModuleType.module3: return const DetailListModule3(); case DetailModuleType.module4: return const DetailListModule4(); case DetailModuleType.module5: return const DetailListModule5(); case DetailModuleType.module6: return const DetailListModule6(); case DetailModuleType.module7: return const DetailListModule7(); case DetailModuleType.module8: return const DetailListModule8(); } }, separatorBuilder: (context, index) { return Container( color: Colors.black12, height: 1, ); }, itemCount: moduleTypes.length, // Set a large enough cacheExtent to ensure that the keep position // functionality can work properly. // // More information and tips: // https://github.com/fluttercandies/flutter_scrollview_observer/wiki/3%E3%80%81Chat-Observer // // Since the content of the current page is small, maxFinite is set up. cacheExtent: double.maxFinite, ); resultWidget = ListViewObserver( controller: state.observerController, dynamicLeadingOffset: () => state.navBarHeight, onObserve: logic.onObserveForListView, scrollNotificationPredicate: defaultScrollNotificationPredicate, child: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/detail/widget/detail_nav_bar.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-03 16:59:04 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic_nav_bar.dart'; import 'package:scrollview_observer_example/features/scene/detail/state/detail_state.dart'; class DetailNavBar extends StatefulWidget { const DetailNavBar({super.key}); @override State createState() => _DetailNavBarState(); } class _DetailNavBarState extends State with DetailLogicConsumerMixin { DetailState get state => logic.state; @override Widget build(BuildContext context) { return GetBuilder( tag: logicTag, id: DetailUpdateType.navBar, builder: (_) { return _buildBody(); }, ); } Widget _buildBody() { Widget resultWidget = Container( height: state.navBarHeight, decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: state.navBarAlpha > 0 ? Colors.black.withValues(alpha: 0.05) : Colors.transparent, offset: const Offset(0, 2), blurRadius: 4, ), ], ), child: _buildTabBar(), ); resultWidget = Opacity( opacity: state.navBarAlpha, child: resultWidget, ); resultWidget = IgnorePointer( ignoring: state.navBarAlpha < 0.2, child: resultWidget, ); return resultWidget; } Widget _buildTabBar() { final navBarTabController = state.navBarTabController; if (navBarTabController == null) return const SizedBox(); return TabBar( tabAlignment: TabAlignment.start, isScrollable: true, tabs: state.navBarTabs.map((e) => Tab(text: e.title)).toList(), controller: navBarTabController, indicatorColor: Colors.blueAccent, indicatorWeight: 3.0, indicatorSize: TabBarIndicatorSize.label, labelColor: Colors.black, unselectedLabelColor: Colors.grey[600], labelStyle: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, ), unselectedLabelStyle: const TextStyle(fontSize: 14.0), labelPadding: const EdgeInsets.only(right: 15), padding: const EdgeInsets.symmetric(horizontal: 8), overlayColor: WidgetStateProperty.all(Colors.transparent), dividerColor: Colors.transparent, onTap: logic.handleNavBarTabTap, ); } } ================================================ FILE: example/lib/features/scene/detail/widget/list_item/detail_list_module1.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 21:32:17 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_list_item_wrapper.dart'; class DetailListModule1 extends StatefulWidget { const DetailListModule1({super.key}); @override State createState() => _DetailListModule1State(); } class _DetailListModule1State extends State with DetailLogicConsumerMixin { @override Widget build(BuildContext context) { Widget resultWidget = _buildPageView(); resultWidget = DetailListItemWrapper( child: resultWidget, ); return resultWidget; } Widget _buildPageView() { Widget resultWidget = PageView.builder( itemCount: 5, itemBuilder: (context, index) { return _buildPageItem(index); }, ); resultWidget = SizedBox( height: 200, child: resultWidget, ); return resultWidget; } Widget _buildPageItem(int index) { Widget resultWidget = Stack( children: [ _buildPageItemBody(index), Positioned( top: 16, right: 16, child: _buildPageItemIndex(index), ), ], ); resultWidget = Container( margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: Colors.purple.shade100, borderRadius: BorderRadius.circular(16), ), child: resultWidget, ); return resultWidget; } Widget _buildPageItemBody(int index) { Widget resultWidget = Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Image Title ${index + 1}', style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, shadows: [ Shadow( blurRadius: 4.0, color: Colors.black, offset: Offset(2.0, 2.0), ), ], ), ), const SizedBox(height: 4), Text( 'This is a description for image ${index + 1}.', style: const TextStyle( color: Colors.white70, fontSize: 14, shadows: [ Shadow( blurRadius: 4.0, color: Colors.black, offset: Offset(2.0, 2.0), ), ], ), ), ], ); resultWidget = Padding( padding: const EdgeInsets.all(16), child: resultWidget, ); return resultWidget; } Widget _buildPageItemIndex(int index) { Widget resultWidget = Text( '${index + 1}/5', style: const TextStyle( color: Colors.white, fontSize: 12, ), ); resultWidget = Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(8), ), child: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/detail/widget/list_item/detail_list_module2.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 21:32:08 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_list_item_wrapper.dart'; class DetailListModule2 extends StatefulWidget { const DetailListModule2({super.key}); @override State createState() => _DetailListModule2State(); } class _DetailListModule2State extends State with DetailLogicConsumerMixin { @override Widget build(BuildContext context) { Widget resultWidget = _buildListView(); resultWidget = SizedBox( height: 300, child: resultWidget, ); resultWidget = DetailListItemWrapper( title: 'Module 2', child: resultWidget, ); return resultWidget; } Widget _buildListView() { return ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 8), itemCount: 5, itemBuilder: (context, index) { return _buildItem(index); }, separatorBuilder: (context, index) { return const SizedBox(width: 8); }, ); } Widget _buildItem(int index) { Widget resultWidget = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( children: [ _buildItemCover(index), Positioned( top: 8, right: 8, child: _buildItemFavoriteIcon(), ), ], ), Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildItemTitle(), const SizedBox(height: 8), _buildItemBrand(), const SizedBox(height: 40), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildItemPrice(), _buildItemAddBtn(), ], ), ], ), ), ], ); resultWidget = SizedBox( width: 200, child: resultWidget, ); resultWidget = Card( margin: const EdgeInsets.symmetric(vertical: 5), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0), ), elevation: 2, child: resultWidget, ); return resultWidget; } Widget _buildItemBrand() { return const Text( 'NIKE', style: TextStyle( color: Colors.grey, fontSize: 12, ), ); } Widget _buildItemTitle() { return const Text( 'Air Force 1', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ); } Widget _buildItemAddBtn() { return Container( decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.add, color: Colors.white, size: 24, ), ); } Widget _buildItemPrice() { return const Text( '\$90.00', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ); } Widget _buildItemFavoriteIcon() { Widget resultWidget = const Icon( Icons.favorite, color: Colors.red, size: 20, ); resultWidget = Container( decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), padding: const EdgeInsets.all(4.0), child: resultWidget, ); return resultWidget; } Widget _buildItemCover(int index) { return Container( height: 150, alignment: Alignment.center, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: const BorderRadius.vertical( top: Radius.circular(16.0), ), ), ); } } ================================================ FILE: example/lib/features/scene/detail/widget/list_item/detail_list_module3.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 21:32:21 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic.dart'; import 'package:scrollview_observer_example/features/scene/detail/state/detail_state.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_list_item_wrapper.dart'; class DetailListModule3 extends StatefulWidget { const DetailListModule3({super.key}); @override State createState() => _DetailListModule3State(); } class _DetailListModule3State extends State with DetailLogicConsumerMixin { DetailState get state => logic.state; List icons = [ Icons.flight, Icons.restaurant, Icons.shopping_bag, Icons.local_gas_station, Icons.movie, Icons.sports_soccer, Icons.music_note, Icons.fitness_center, ]; List titles = [ 'Flight', 'Food', 'Shopping', 'Fuel', 'Movie', 'Sports', 'Music', 'Fitness', ]; @override Widget build(BuildContext context) { return GetBuilder( tag: logicTag, id: DetailUpdateType.module3, builder: (_) { if (!state.haveDataForModule3) return const SizedBox.shrink(); Widget resultWidget = _buildGridView(); resultWidget = DetailListItemWrapper( title: 'Module 3', child: resultWidget, ); return resultWidget; }, ); } Widget _buildGridView() { return GridView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, padding: const EdgeInsets.all(16), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, crossAxisSpacing: 10, mainAxisSpacing: 10, childAspectRatio: 0.8, ), itemCount: 8, itemBuilder: (context, index) { return _buildGridItem(index); }, ); } Widget _buildGridItem(int index) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildItemIcon(index), const SizedBox(height: 8), _buildItemTitle(index), ], ); } Widget _buildItemTitle(int index) { return Text( titles[index], style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ); } Widget _buildItemIcon(int index) { Widget resultWidget = Icon( icons[index], color: Colors.blue.shade800, size: 30, ); resultWidget = Container( width: 60, height: 60, decoration: BoxDecoration( color: Colors.blue.shade100, shape: BoxShape.circle, ), child: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/detail/widget/list_item/detail_list_module4.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 21:30:01 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_list_item_wrapper.dart'; class DetailListModule4 extends StatefulWidget { const DetailListModule4({super.key}); @override State createState() => _DetailListModule4State(); } class _DetailListModule4State extends State with DetailLogicConsumerMixin { @override Widget build(BuildContext context) { Widget resultWidget = _buildListView(); resultWidget = DetailListItemWrapper( title: 'Module 4', child: resultWidget, ); return resultWidget; } Widget _buildListView() { return ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, padding: const EdgeInsets.symmetric(vertical: 8), itemBuilder: (context, index) { return _buildItem(index); }, itemCount: 5, ); } Widget _buildItem(int index) { Widget resultWidget = Row( children: [ _buildItemAvatar(), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildItemTitle(index), const SizedBox(height: 4), _buildItemSubtitle(index), ], ), ), _buildItemArrow(), ], ); resultWidget = Container( margin: const EdgeInsets.symmetric( horizontal: 8, vertical: 8, ), padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: const [ BoxShadow( color: Colors.grey, spreadRadius: 1, blurRadius: 3, offset: Offset(0, 1), ), ], ), child: resultWidget, ); return resultWidget; } Widget _buildItemSubtitle(int index) { return Text( 'Software Engineer - Company ${index + 1}', style: const TextStyle( color: Colors.grey, fontSize: 14, ), ); } Widget _buildItemTitle(int index) { return Text( 'User Name ${index + 1}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ); } Widget _buildItemAvatar() { return CircleAvatar( radius: 30, backgroundColor: Colors.blue.shade100, child: Icon( Icons.person, size: 30, color: Colors.blue.shade800, ), ); } Widget _buildItemArrow() { return Icon( Icons.arrow_forward_ios, color: Colors.grey.shade400, ); } } ================================================ FILE: example/lib/features/scene/detail/widget/list_item/detail_list_module5.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 21:32:26 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_list_item_wrapper.dart'; class DetailListModule5 extends StatefulWidget { const DetailListModule5({super.key}); @override State createState() => _DetailListModule5State(); } class _DetailListModule5State extends State with DetailLogicConsumerMixin { List titles = [ 'Enable Notifications', 'Dark Mode', 'Autoplay Videos', 'Remember Me', 'Privacy Settings', ]; List subtitles = [ 'Receive app notifications', 'Switch app theme to dark', 'Autoplay videos on Wi-Fi', 'Automatically log in next time', 'Manage your privacy preferences', ]; @override Widget build(BuildContext context) { Widget resultWidget = _buildListView(); resultWidget = DetailListItemWrapper( title: 'Module 5', child: resultWidget, ); return resultWidget; } Widget _buildListView() { return ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: titles.length, itemBuilder: (context, index) { return Column( children: [ _buildListItem(index), if (index < titles.length - 1) const Divider(height: 1), ], ); }, ); } Widget _buildListItem(int index) { return ListTile( leading: _getLeadingIcon(index), title: Text(titles[index]), subtitle: Text(subtitles[index]), trailing: _getTrailingWidget(index), ); } Widget _getLeadingIcon(int index) { IconData iconData; switch (index) { case 0: iconData = Icons.notifications; break; case 1: iconData = Icons.dark_mode; break; case 2: iconData = Icons.play_circle_fill; break; case 3: iconData = Icons.lock; break; case 4: iconData = Icons.privacy_tip; break; default: iconData = Icons.settings; break; } return Icon(iconData); } Widget _getTrailingWidget(int index) { return Switch( value: index % 2 == 0, onChanged: (_) {}, ); } } ================================================ FILE: example/lib/features/scene/detail/widget/list_item/detail_list_module6.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 21:32:31 */ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/logic/detail_logic.dart'; import 'package:scrollview_observer_example/features/scene/detail/state/detail_state.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_list_item_wrapper.dart'; class DetailListModule6 extends StatefulWidget { const DetailListModule6({super.key}); @override State createState() => _DetailListModule6State(); } class _DetailListModule6State extends State with DetailLogicConsumerMixin { DetailState get state => logic.state; @override Widget build(BuildContext context) { return GetBuilder( tag: logicTag, id: DetailUpdateType.module6, builder: (_) { if (!state.haveDataForModule6) return const SizedBox.shrink(); Widget resultWidget = _buildListView(); resultWidget = DetailListItemWrapper( title: 'Module 6', child: resultWidget, ); return resultWidget; }, ); } Widget _buildListView() { Widget resultWidget = ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 8), itemCount: 5, itemBuilder: (context, index) { return _buildCardItem(index); }, ); resultWidget = SizedBox( height: 220, child: resultWidget, ); return resultWidget; } Widget _buildCardItem(int index) { Widget resultWidget = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildCardItemImage(), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildCardItemTitle(index), const SizedBox(height: 4), _buildCardItemSubtitle(index), ], ), ], ); resultWidget = SizedBox( width: 180, child: resultWidget, ); resultWidget = Card( elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: resultWidget, ); resultWidget = Padding( padding: EdgeInsets.zero, child: resultWidget, ); return resultWidget; } Widget _buildCardItemImage() { return Container( height: 120, width: double.infinity, decoration: BoxDecoration( color: Colors.blueGrey.shade100, borderRadius: const BorderRadius.vertical( top: Radius.circular(12), ), ), ); } Widget _buildCardItemSubtitle(int index) { Widget resultWidget = Text( 'Description for item ${index + 1}.', style: const TextStyle( color: Colors.grey, fontSize: 12, ), maxLines: 2, overflow: TextOverflow.ellipsis, ); resultWidget = Padding( padding: const EdgeInsets.all(8.0), child: resultWidget, ); return resultWidget; } Widget _buildCardItemTitle(int index) { Widget resultWidget = Text( 'Title ${index + 1}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), maxLines: 1, overflow: TextOverflow.ellipsis, ); resultWidget = Padding( padding: const EdgeInsets.all(8.0), child: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/detail/widget/list_item/detail_list_module7.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 21:32:39 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_list_item_wrapper.dart'; class DetailListModule7 extends StatefulWidget { const DetailListModule7({super.key}); @override State createState() => _DetailListModule7State(); } class _DetailListModule7State extends State with DetailLogicConsumerMixin { @override Widget build(BuildContext context) { Widget resultWidget = _buildListView(); resultWidget = DetailListItemWrapper( title: 'Module 7', child: resultWidget, ); return resultWidget; } Widget _buildListView() { return ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, padding: const EdgeInsets.all(8), itemCount: 5, itemBuilder: (context, index) { return _buildListItem(index); }, ); } Widget _buildListItem(int index) { double progress = (index + 1) * 0.2; Widget resultWidget = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildListItemTitle(index), const SizedBox(height: 8), LinearProgressIndicator( value: progress, backgroundColor: Colors.grey.shade300, valueColor: const AlwaysStoppedAnimation(Colors.blue), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildListItemProgress(progress), _buildListItemStatus(progress), ], ), ], ); resultWidget = Padding( padding: const EdgeInsets.all(16.0), child: resultWidget, ); resultWidget = Card( margin: const EdgeInsets.symmetric(vertical: 8), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: resultWidget, ); return resultWidget; } Widget _buildListItemStatus(double progress) { return Text( progress < 1.0 ? 'In Progress' : 'Completed', style: TextStyle( color: progress < 1.0 ? Colors.orange : Colors.green, fontWeight: FontWeight.bold, ), ); } Widget _buildListItemProgress(double progress) { return Text( '${(progress * 100).toInt()}% Done', style: const TextStyle( color: Colors.grey, fontSize: 14, ), ); } Widget _buildListItemTitle(int index) { return Text( 'Task ${index + 1}: Data Sync', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ); } } ================================================ FILE: example/lib/features/scene/detail/widget/list_item/detail_list_module8.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2025-08-02 21:32:45 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/detail/header/detail_header.dart'; import 'package:scrollview_observer_example/features/scene/detail/widget/detail_list_item_wrapper.dart'; class DetailListModule8 extends StatefulWidget { const DetailListModule8({super.key}); @override State createState() => _DetailListModule8State(); } class _DetailListModule8State extends State with DetailLogicConsumerMixin { @override Widget build(BuildContext context) { Widget resultWidget = _buildListView(); resultWidget = DetailListItemWrapper( title: 'Module 8', child: resultWidget, ); return resultWidget; } Widget _buildListView() { return ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, padding: const EdgeInsets.symmetric(vertical: 8), itemBuilder: (context, index) { return _buildItem(index); }, itemCount: 5, ); } Widget _buildItem(int index) { Widget resultWidget = Row( children: [ _buildItemImage(), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildItemDetails(), const SizedBox(height: 16), ], ), ), ], ); resultWidget = Padding( padding: const EdgeInsets.all(8.0), child: resultWidget, ); resultWidget = Card( margin: const EdgeInsets.symmetric( horizontal: 8, vertical: 8, ), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0), ), child: resultWidget, ); return resultWidget; } Widget _buildItemImage() { return Container( width: 80, height: 80, decoration: BoxDecoration( color: Colors.blueGrey, borderRadius: BorderRadius.circular(12), ), ); } Widget _buildItemDetails() { return const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Air Jordan 1 Low SE', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), SizedBox(height: 4), Text( 'NIKE', style: TextStyle( color: Colors.grey, fontSize: 12, ), ), SizedBox(height: 4), Text( '\$120.00', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), ], ); } } ================================================ FILE: example/lib/features/scene/expandable_carousel_slider_demo/expandable_carousel_slider_demo.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-11-25 20:15:18 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:carousel_slider/utils.dart'; import 'package:collection/collection.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; class ExpandableCarouselSliderDemo extends StatefulWidget { const ExpandableCarouselSliderDemo({Key? key}) : super(key: key); @override State createState() => _ExpandableCarouselSliderDemoState(); } class _ExpandableCarouselSliderDemoState extends State { List imgIdList = [ 'photo-1732282537685-bec9036bf4e0', 'photo-1732418313819-329bc187bab7', 'photo-1732135250211-5009233cee37', 'photo-1731143061417-964b0768bd22', 'photo-1731949594994-739b3ec954ef', 'photo-1731773287304-3306a88f1e90', ]; List itemHeightList = [500, 400, 300, 500, 400, 300]; late ValueNotifier carouselHeight; List itemWidgetList = []; final GlobalKey _carouselKey = GlobalKey(); late final observerController = ListObserverController() ..observeIntervalForScrolling = const Duration(milliseconds: 1); @override void initState() { super.initState(); carouselHeight = ValueNotifier(itemHeightList.first); itemWidgetList = imgIdList.mapIndexed((index, imgId) { return CarouselItem( index: index, height: itemHeightList[index], imgId: imgId, ); }).toList(); } @override void dispose() { carouselHeight.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Expandable Carousel Slider'), ), body: Column( children: [ _buildCarousel(), const SizedBox(height: 20), _buildIndicator(), ], ), ); } Widget _buildIndicator() { return Container( width: 200, height: 30, alignment: Alignment.center, decoration: const BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.all(Radius.circular(15)), ), child: const Text( 'I am an indicator', style: TextStyle( color: Colors.white, ), ), ); } Widget _buildCarousel() { Widget resultWidget = ValueListenableBuilder( valueListenable: carouselHeight, builder: (context, value, child) { return CarouselSlider( key: _carouselKey, options: CarouselOptions( viewportFraction: 1, height: carouselHeight.value, autoPlay: true, autoPlayInterval: const Duration(seconds: 4), onPageChanged: (index, reason) {}, ), items: itemWidgetList, ); }, ); resultWidget = ListViewObserver( controller: observerController, child: resultWidget, triggerOnObserveType: ObserverTriggerOnObserveType.directly, customTargetRenderSliverType: (renderObj) { return renderObj is RenderSliverFillViewport; }, onObserve: (result) { final carouselState = _carouselKey.currentState?.carouselState; if (carouselState == null) return; if (result.displayingChildModelList.length < 2) return; final firstChild = result.displayingChildModelList.first; final firstChildIndex = firstChild.index; // Get the real index of the first child. final firstChildRealIndex = getRealIndex( firstChildIndex + carouselState.initialPage, carouselState.realPage, imgIdList.length, ); // Get the real index of the second child. int secondChildRealIndex = firstChildRealIndex + 1; // Reset to 0 if exceeds the range. if (secondChildRealIndex >= imgIdList.length) { secondChildRealIndex = 0; } final firstChildLeadingMarginToViewport = firstChild.leadingMarginToViewport; final viewportMainAxisExtent = firstChild.viewportMainAxisExtent; final firstChildHeight = itemHeightList[firstChildRealIndex]; final secondChildHeight = itemHeightList[secondChildRealIndex]; final progress = (firstChildLeadingMarginToViewport.abs() / viewportMainAxisExtent) .clamp(0.0, 1.0); carouselHeight.value = firstChildHeight - ((firstChildHeight - secondChildHeight) * progress); }, ); return resultWidget; } } class CarouselItem extends StatelessWidget { const CarouselItem({ Key? key, required this.index, required this.height, required this.imgId, }) : super(key: key); final int index; final double height; final String imgId; @override Widget build(BuildContext context) { return Stack( children: [ Positioned( left: 0, right: 0, top: 0, child: Container( width: double.infinity, height: height, child: Image.network( // 'https://picsum.photos/${MediaQuery.sizeOf(context).width.toInt()}/${height.toInt()}?random=$index', 'https://images.unsplash.com/$imgId?auto=format&fit=crop&w=${MediaQuery.sizeOf(context).width.toInt()}&h=${height.toInt()}&q=100', ), color: index % 2 == 0 ? Colors.red : Colors.amber, ), ), ], ); } } ================================================ FILE: example/lib/features/scene/image_tab_demo/image_tab_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-22 22:17:55 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; class ImageTabPage extends StatefulWidget { const ImageTabPage({Key? key}) : super(key: key); @override State createState() => _ImageTabPageState(); } class _ImageTabPageState extends State { final ValueNotifier selectedIndex = ValueNotifier(0); final ScrollController scrollController = ScrollController(); late ListObserverController observerController; double screenWidth = 0; List imgUrlList = [ 'photo-1660806982611-0a41c0527966', 'photo-1660032356057-efd3e1eb045c', 'photo-1660139099083-03e0777ac6a7', 'photo-1659030320611-9d23ca40e29e', 'photo-1658858288004-42989dac61a2', 'photo-1647238384941-adfd9288341b', 'photo-1651054558996-03455fe2702f', 'photo-1655704705321-3ac52dc67f70', 'photo-1654250910768-0162e080ef86', 'photo-1652956815155-5c54d1fc40a9', ]; @override void initState() { super.initState(); observerController = ListObserverController(controller: scrollController); } @override void dispose() { observerController.controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; return Scaffold( appBar: AppBar(title: const Text("ImageTab")), body: Column( children: [ SizedBox( width: double.infinity, height: 300, child: ValueListenableBuilder( builder: (BuildContext context, int value, Widget? child) { return _buildImageWidget(value); }, valueListenable: selectedIndex, ), ), const SizedBox(height: 10), _buildImageTabBar(), ], ), ); } Widget _buildImageTabBar() { Widget resultWidget = ListViewObserver( controller: observerController, child: ListView.separated( controller: scrollController, itemBuilder: (ctx, index) { return _buildListItemView(index); }, separatorBuilder: (ctx, index) { return _buildSeparatorView(); }, itemCount: imgUrlList.length, scrollDirection: Axis.horizontal, ), ); resultWidget = SizedBox( height: 100, child: resultWidget, ); return resultWidget; } Widget _buildListItemView(int index) { Widget imgWidget = _buildImageWidget(index); Widget resultWidget = ValueListenableBuilder( builder: (BuildContext context, int value, Widget? child) { return Container( decoration: BoxDecoration( border: (selectedIndex.value == index) ? Border.all(color: Colors.orange, width: 3) : null, ), width: 80, height: 50, child: imgWidget, ); }, valueListenable: selectedIndex, ); resultWidget = GestureDetector( child: resultWidget, onTap: () { debugPrint('index -- $index'); selectedIndex.value = index; observerController.animateTo( index: index, alignment: 0.5, duration: const Duration(milliseconds: 250), curve: Curves.ease, offset: (_) { return screenWidth * 0.5; }, ); }, ); return resultWidget; } Widget _buildImageWidget(int index) { return Image.network( _fetchImgUrl(index), fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( alignment: Alignment.center, child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ); }, ); } Container _buildSeparatorView() { return Container( color: Colors.white, width: 5, ); } String _fetchImgUrl(int index) { return 'https://images.unsplash.com/' + imgUrlList[index] + '?auto=format&fit=crop&w=375&q=100'; } } ================================================ FILE: example/lib/features/scene/scrollview_form_demo/scrollview_form_demo_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-08-10 22:35:59 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/utils/random.dart'; class ScrollViewFormDemoPage extends StatefulWidget { const ScrollViewFormDemoPage({Key? key}) : super(key: key); @override State createState() => _ScrollViewFormDemoPageState(); } class _ScrollViewFormDemoPageState extends State { ScrollController scrollController = ScrollController(); late ListObserverController observerController; final int formIndex = 3; FocusNode formFocusNode = FocusNode(); List imgUrlList = [ 'photo-1542317858-043bf4d34f9f', 'photo-1472053217156-31b42df2319c', 'photo-1643727349026-ba9c33acf10a', 'photo-1615678857339-4e7e51ce22db', 'photo-1647772809611-cd884d37de42', 'photo-1505329603060-7c67cc801ee5', 'photo-1627308722931-0e6a1214c03e', 'photo-1556799483-e642a45aeb68', ]; handleFormFocus() async { if (!formFocusNode.hasFocus) return; // Wait for the keyboard to fully display. await Future.delayed(const Duration(milliseconds: 600)); // Trigger the observer to observe the ListView. final result = await observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); if (!result.isSuccess) return; // Find the observation result for the form item. final formResultModel = result.observeResult?.displayingChildModelList.firstWhere((element) { return element.index == formIndex; }); if (formResultModel == null) return; // Let the bottom of the form item view be fully displayed. observerController.controller?.animateTo( formResultModel.scrollOffset - formResultModel.trailingMarginToViewport, duration: const Duration(milliseconds: 200), curve: Curves.ease, ); } @override void initState() { super.initState(); observerController = ListObserverController(controller: scrollController); formFocusNode.addListener(handleFormFocus); } @override void dispose() { formFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar(title: const Text("ScrollView Form")), body: _buildScrollView(), ); } Widget _buildScrollView() { Widget resultWidget = ListView.builder( controller: scrollController, itemBuilder: (context, index) { if (formIndex == index) { return _buildForm(); } return _buildImage(); }, itemCount: 10, ); resultWidget = ListViewObserver( controller: observerController, autoTriggerObserveTypes: const [], child: resultWidget, ); return resultWidget; } Widget _buildForm() { Widget resultWidget = Form( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Feedback', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), TextField( focusNode: formFocusNode, ), Container( width: double.infinity, color: Colors.white, alignment: Alignment.center, margin: const EdgeInsets.only(top: 10.0), child: TextButton( child: const Text('Submit'), onPressed: () { formFocusNode.unfocus(); }, ), ), ], ), ); resultWidget = Container( color: Colors.green[100], padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 30), child: resultWidget, ); return resultWidget; } Widget _buildImage() { Widget resultWidget = Image.network( _fetchImgUrl(), fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( alignment: Alignment.center, child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ); }, ); resultWidget = Container( margin: const EdgeInsets.all(10), child: resultWidget, ); return resultWidget; } String _fetchImgUrl() { return 'https://images.unsplash.com/' + imgUrlList[RandomTool.genInt(max: imgUrlList.length)] + '?auto=format&fit=crop&w=375&q=100'; } } ================================================ FILE: example/lib/features/scene/video_auto_play_list/video_list_auto_play_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-07-03 15:46:45 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/scene/video_auto_play_list/widgets/video_widget.dart'; class VideoListAutoPlayPage extends StatefulWidget { const VideoListAutoPlayPage({Key? key}) : super(key: key); @override State createState() => _VideoListAutoPlayPageState(); } class _VideoListAutoPlayPageState extends State { BuildContext? _ctx1; int _hitIndex = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Video Auto Play")), body: ListViewObserver( child: _buildListView(), sliverListContexts: () { return [if (_ctx1 != null) _ctx1!]; }, onObserveAll: (resultMap) { final model = resultMap[_ctx1]; if (model == null) return; if (_hitIndex != model.firstChild?.index) { _hitIndex = model.firstChild?.index ?? 0; setState(() {}); } }, leadingOffset: 200, ), ); } ListView _buildListView() { return ListView.separated( itemBuilder: (ctx, index) { _ctx1 = ctx; return _buildListItemView(index); }, separatorBuilder: (ctx, index) { return _buildSeparatorView(); }, itemCount: 50, ); } Widget _buildListItemView(int index) { return SizedBox( height: 300, child: _hitIndex == index ? const VideoWidget( url: 'https://www.w3schools.com/html/movie.mp4', ) : Container( color: Colors.blue[100], child: const Center(child: Text('placeholder')), ), ); } Container _buildSeparatorView() { return Container( color: Colors.white, height: 8, ); } } ================================================ FILE: example/lib/features/scene/video_auto_play_list/widgets/video_widget.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-05-28 14:08:53 */ import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; class VideoWidget extends StatefulWidget { final String url; const VideoWidget({ Key? key, required this.url, }) : super(key: key); @override State createState() => _VideoWidgetState(); } class _VideoWidgetState extends State { late VideoPlayerController _controller; late Future _initializeVideoPlayerFuture; @override void initState() { super.initState(); _controller = VideoPlayerController.networkUrl(Uri.parse(widget.url)); _initializeVideoPlayerFuture = _controller.initialize().then((_) { setState(() {}); }); _controller.play(); _controller.setLooping(true); } @override void dispose() { _controller.dispose(); super.dispose(); } @override void didUpdateWidget(VideoWidget oldWidget) { _controller.play(); _controller.setLooping(true); super.didUpdateWidget(oldWidget); } @override Widget build(BuildContext context) { return FutureBuilder( future: _initializeVideoPlayerFuture, builder: (ctx, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return VideoPlayer(_controller); } return const Center(child: CircularProgressIndicator()); }, ); } } ================================================ FILE: example/lib/features/scene/visibility_demo/mixin/visibility_exposure_mixin.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-08-24 22:55:19 */ import 'package:scrollview_observer/scrollview_observer.dart'; mixin VisibilityExposureMixin { // Exposure record Map exposureRecordMap = {}; /// Reset exposure record resetExposureRecordMap() { exposureRecordMap.clear(); } /// Handling the exposure logic of ScrollView item /// /// [resultModel] Observation result (the super class is ObserveModel, /// pass the value obtained in the onObserve callback, or the value obtained /// according to BuildContext in onObserveAll). /// [toExposeDisplayPercent] When the self-display ratio exceeds this value, /// it is regarded as exposure and recorded, otherwise the exposure record /// is reset. /// [recordKeyCallback] Return the key used to record the exposure, if not /// implemented, use index as the key. /// [needExposeCallback] Whether to participate in the callback of exposure /// calculation. /// [toExposeCallback] Callback for exposure conditions met. handleExposure({ required dynamic resultModel, double toExposeDisplayPercent = 0.5, dynamic Function(int index)? recordKeyCallback, bool Function(int index)? needExposeCallback, required Function(int index) toExposeCallback, }) { List displayingChildModelList = []; if (resultModel is ListViewObserveModel) { displayingChildModelList = resultModel.displayingChildModelList; } else if (resultModel is GridViewObserveModel) { displayingChildModelList = resultModel.displayingChildModelList; } for (var displayingChildModel in displayingChildModelList) { final index = displayingChildModel.index; final recordKey = recordKeyCallback?.call(index) ?? index; // By letting the outside tell us whether ScrollView item need to // participate in the exposure calculation logic final needExpose = needExposeCallback?.call(index) ?? true; if (!needExpose) continue; // debugPrint('item : $index - ${displayingChildModel.displayPercentage}'); // Determine whether the percentage displayed by the item exceeds // [toExposeDisplayPercent] if (displayingChildModel.displayPercentage < toExposeDisplayPercent) { // Does not meet the exposure conditions, reset exposure record exposureRecordMap[recordKey] = false; } else { // Meet the exposure conditions final haveExposure = exposureRecordMap[recordKey] ?? false; if (haveExposure) continue; toExposeCallback(index); exposureRecordMap[recordKey] = true; } } } } ================================================ FILE: example/lib/features/scene/visibility_demo/page/visibility_listview_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-08-25 23:14:20 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/scene/visibility_demo/mixin/visibility_exposure_mixin.dart'; class VisibilityListViewPage extends StatefulWidget { const VisibilityListViewPage({Key? key}) : super(key: key); @override State createState() => _VisibilityListViewPageState(); } class _VisibilityListViewPageState extends State with VisibilityExposureMixin { int needExposeIndex = 6; final observerController = ListObserverController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Visibility ListView")), body: ListViewObserver( child: _buildListView(), triggerOnObserveType: ObserverTriggerOnObserveType.directly, controller: observerController, onObserve: (resultModel) { // final models = resultModel.displayingChildModelList; // final indexList = models.map((e) => e.index).toList(); // final displayPercentageList = // models.map((e) => e.displayPercentage).toList(); // debugPrint('index -- $indexList -- $displayPercentageList'); handleExposure( resultModel: resultModel, needExposeCallback: (index) { // Only the item whose index is 6 needs to calculate whether it // has been exposed. return index == needExposeIndex; // 6 }, toExposeCallback: (index) { // Meet the conditions, you can report exposure. debugPrint('Exposure -- $index'); }, ); }, ), floatingActionButton: FloatingActionButton( onPressed: () { // Trigger an observation manually observerController.dispatchOnceObserve(); }, ), ); } Widget _buildListView() { return ListView.separated( itemBuilder: (ctx, index) { return _buildListItemView(index); }, separatorBuilder: (ctx, index) { return _buildSeparatorView(); }, itemCount: 100, ); } Widget _buildListItemView(int index) { final isEven = index % 2 == 0; final needExpose = needExposeIndex == index; return Container( height: isEven ? 200 : 100, color: needExpose ? Colors.red : isEven ? Colors.orange[300] : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: needExpose ? Colors.white : Colors.black, ), ), ), ); } Container _buildSeparatorView() { return Container( color: Colors.white, height: 5, ); } } ================================================ FILE: example/lib/features/scene/visibility_demo/page/visibility_scrollview_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-08-25 23:14:20 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/scene/visibility_demo/mixin/visibility_exposure_mixin.dart'; import 'package:visibility_detector/visibility_detector.dart'; class VisibilityScrollViewPage extends StatefulWidget { const VisibilityScrollViewPage({Key? key}) : super(key: key); @override State createState() => _VisibilityScrollViewPageState(); } class _VisibilityScrollViewPageState extends State with VisibilityExposureMixin { BuildContext? _sliverListCtx; BuildContext? _sliverGridCtx; int needExposeIndex = 6; final observerController = SliverObserverController(); @override Widget build(BuildContext context) { return Scaffold( body: SliverViewObserver( controller: observerController, child: _buildScrollView(), sliverContexts: () { return [ if (_sliverListCtx != null) _sliverListCtx!, if (_sliverGridCtx != null) _sliverGridCtx!, ]; }, // autoTriggerObserveTypes: const [ // ObserverAutoTriggerObserveType.scrollEnd, // ], triggerOnObserveType: ObserverTriggerOnObserveType.directly, onObserveAll: (resultMap) { // SliverList final listResultModel = resultMap[_sliverListCtx]; if (listResultModel != null) { handleExposure( resultModel: listResultModel, recordKeyCallback: (index) => 'list_$index', needExposeCallback: (index) { // Only the item whose index is 6 needs to calculate whether it // has been exposed. return index == needExposeIndex; // 6 }, toExposeCallback: (index) { // Meet the conditions, you can report exposure. debugPrint('List Exposure -- $index'); }, ); } // SliverGrid final gridResultModel = resultMap[_sliverGridCtx]; if (gridResultModel != null) { handleExposure( resultModel: gridResultModel, recordKeyCallback: (index) => 'grid_$index', needExposeCallback: (index) { // Only the item whose index is 6 needs to calculate whether it // has been exposed. return index == needExposeIndex; // 6 }, toExposeCallback: (index) { // Meet the conditions, you can report exposure. debugPrint('Grid Exposure -- $index'); }, ); } }, ), ); } Widget _buildScrollView() { return CustomScrollView( slivers: [ _buildSliverAppBar(), _buildSliverListView(), _buildMiddleSliver(), _buildSliverGridView(), ], cacheExtent: double.maxFinite, ); } Widget _buildSliverAppBar() { return SliverAppBar( pinned: true, expandedHeight: 200, flexibleSpace: FlexibleSpaceBar( title: const Text('AppBar'), background: Container(color: Colors.orange), ), ); } Widget _buildSliverListView() { return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { _sliverListCtx ??= ctx; final isEven = index % 2 == 0; final needExpose = index == needExposeIndex; return Container( height: isEven ? 200 : 100, color: needExpose ? Colors.purple : isEven ? Colors.red : Colors.black12, child: Center( child: Text( "index -- $index", style: TextStyle( color: isEven ? Colors.white : Colors.black, ), ), ), ); }, childCount: 10, ), ); } Widget _buildMiddleSliver() { return SliverVisibilityDetector( key: const ValueKey('key'), sliver: SliverToBoxAdapter( child: Container( height: 200, color: Colors.blue, child: const Center( child: Text('Middle Sliver'), ), ), ), onVisibilityChanged: (info) { // In the scene with PersistentHeader (such as AppBar), the value of // [info.visibleFraction] is incorrect. debugPrint('visibleFraction: ${info.visibleFraction}'); }, ); } Widget _buildSliverGridView() { return SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 2.0, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { _sliverGridCtx ??= context; final needExpose = index == needExposeIndex; return Container( color: needExpose ? Colors.purple : Colors.green, child: Center( child: Text( 'index -- $index', style: TextStyle( color: needExpose ? Colors.white : Colors.black, ), ), ), ); }, childCount: 20, ), ); } } ================================================ FILE: example/lib/features/scene/waterfall_flow_demo/waterfall_flow_grid_item_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-06-08 21:59:07 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/video_auto_play_list/widgets/video_widget.dart'; import 'package:scrollview_observer_example/features/scene/waterfall_flow_demo/waterfall_flow_type.dart'; class WaterfallFlowGridItemView extends StatelessWidget { final int selfIndex; final WaterFlowHitType selfType; final int hitIndex; final WaterFlowHitType hitType; bool get isHit => selfType == hitType && selfIndex == hitIndex; const WaterfallFlowGridItemView({ Key? key, required this.selfIndex, required this.selfType, required this.hitIndex, required this.hitType, }) : super(key: key); @override Widget build(BuildContext context) { return Container( alignment: Alignment.center, color: isHit ? Colors.amber : Colors.amber[100], child: _buildBody(), // Text('grid item $selfIndex'), // height: 300, ); } Widget _buildBody() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ isHit ? _buildVideo() : _buildCover(), const SizedBox(height: 10), Text('grid item $selfIndex'), SizedBox( height: 50.0 + 50.0 * (selfIndex % 2), ), ], ); } Widget _buildCover() { return Image.network( 'https://images.unsplash.com/photo-1660139099083-03e0777ac6a7?auto=format&fit=crop&w=375&q=100', fit: BoxFit.fitWidth, width: double.infinity, height: 100, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( height: 50, alignment: Alignment.center, child: SizedBox.square( dimension: 20, child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ), ); }, ); } Widget _buildVideo() { Widget resultWidget = const VideoWidget( url: 'https://www.w3schools.com/html/movie.mp4', ); resultWidget = SizedBox( width: double.infinity, height: 100, child: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/waterfall_flow_demo/waterfall_flow_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-05-14 16:22:36 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/features/scene/waterfall_flow_demo/waterfall_flow_grid_item_view.dart'; import 'package:scrollview_observer_example/features/scene/waterfall_flow_demo/waterfall_flow_swipe_view.dart'; import 'package:scrollview_observer_example/features/scene/waterfall_flow_demo/waterfall_flow_type.dart'; import 'package:waterfall_flow/waterfall_flow.dart'; class WaterfallFlowPage extends StatefulWidget { const WaterfallFlowPage({Key? key}) : super(key: key); @override State createState() => WaterfallFlowPageState(); } class WaterfallFlowPageState extends State { BuildContext? grid1Context; BuildContext? grid2Context; BuildContext? swipeContext; BuildContext? firstChildCtxInViewport; bool isRemoveSwipe = false; int hitIndex = 0; WaterFlowHitType hitType = WaterFlowHitType.firstGrid; double observeOffset = 150; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Waterfall Flow')), body: SliverViewObserver( child: _buildBody(), leadingOffset: observeOffset, scrollNotificationPredicate: defaultScrollNotificationPredicate, autoTriggerObserveTypes: const [ ObserverAutoTriggerObserveType.scrollEnd, ], triggerOnObserveType: ObserverTriggerOnObserveType.directly, extendedHandleObserve: (context) { // An extension of the original observation logic. final _obj = ObserverUtils.findRenderObject(context); if (_obj is RenderSliverWaterfallFlow) { return ObserverCore.handleGridObserve( context: context, fetchLeadingOffset: () => observeOffset, ); } return null; }, // customHandleObserve: (context) { // // Here you can customize the observation logic. // final _obj = ObserverUtils.findRenderObject(context); // if (_obj is RenderSliverList) { // ObserverCore.handleListObserve(context: context); // } // if (_obj is RenderSliverGrid || _obj is RenderSliverWaterfallFlow) { // return ObserverCore.handleGridObserve(context: context); // } // return null; // }, sliverContexts: () { return [ if (grid1Context != null) grid1Context!, if (swipeContext != null) swipeContext!, if (grid2Context != null) grid2Context!, ]; }, onObserveViewport: (result) { firstChildCtxInViewport = result.firstChild.sliverContext; if (firstChildCtxInViewport == grid1Context) { debugPrint('current first sliver in viewport - gridView1'); if (WaterFlowHitType.firstGrid == hitType) return; hitType = WaterFlowHitType.firstGrid; hitIndex = -1; } else if (firstChildCtxInViewport == swipeContext) { debugPrint('current first sliver in viewport - swipeView'); if (WaterFlowHitType.swipe == hitType) return; setState(() { hitType = WaterFlowHitType.swipe; }); } else if (firstChildCtxInViewport == grid2Context) { debugPrint('current first sliver in viewport - gridView2'); if (WaterFlowHitType.secondGrid == hitType) return; hitType = WaterFlowHitType.secondGrid; hitIndex = -1; } }, onObserveAll: (resultMap) { final result = resultMap[firstChildCtxInViewport]; if (firstChildCtxInViewport == grid1Context) { if (WaterFlowHitType.firstGrid != hitType) return; if (result == null || result is! GridViewObserveModel) return; final firstIndexList = result.firstGroupChildList.map((e) { return e.index; }).toList(); handleGridHitIndex(firstIndexList); } else if (firstChildCtxInViewport == swipeContext) { } else if (firstChildCtxInViewport == grid2Context) { if (WaterFlowHitType.secondGrid != hitType) return; if (result == null || result is! GridViewObserveModel) return; final firstIndexList = result.firstGroupChildList.map((e) { return e.index; }).toList(); handleGridHitIndex(firstIndexList); } }, ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.swipe), onPressed: () { setState(() { isRemoveSwipe = !isRemoveSwipe; }); }, ), ); } handleGridHitIndex(List firstIndexList) { if (firstIndexList.isEmpty) return; // debugPrint('gridContext displaying -- $firstIndexList'); int targetIndex = firstIndexList.indexOf(hitIndex); if (targetIndex == -1) { targetIndex = 0; } else { targetIndex = targetIndex + 1; if (targetIndex >= firstIndexList.length) { targetIndex = 0; } } setState(() { hitIndex = firstIndexList[targetIndex]; }); } Widget _buildScrollView() { return CustomScrollView( slivers: [ _buildBanner(), _buildSeparator(8), _buildGridView(isFirst: true, childCount: 5), _buildSeparator(8), _buildSwipeView(), _buildSeparator(15), _buildGridView(isFirst: false, childCount: 20), ], ); } Widget _buildBody() { Widget resultWidget = Stack( children: [ _buildScrollView(), Positioned( left: 0, right: 0, height: 1, top: observeOffset, child: Container(color: Colors.red), ), ], ); return resultWidget; } Widget _buildBanner() { return SliverToBoxAdapter( child: Container( color: Colors.green, height: 120, child: const Center( child: Text( 'Banner', style: TextStyle(fontSize: 20, color: Colors.white), ), ), ), ); } Widget _buildGridView({ bool isFirst = false, required int childCount, }) { return SliverWaterfallFlow( gridDelegate: const SliverWaterfallFlowDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 15, crossAxisSpacing: 10, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { WaterFlowHitType selfType; if (isFirst) { if (grid1Context != context) grid1Context = context; selfType = WaterFlowHitType.firstGrid; } else { if (grid2Context != context) grid2Context = context; selfType = WaterFlowHitType.secondGrid; } return WaterfallFlowGridItemView( selfIndex: index, selfType: selfType, hitIndex: hitIndex, hitType: hitType, ); }, childCount: childCount, ), ); } Widget _buildSwipeView() { if (isRemoveSwipe) { swipeContext = null; return const SliverToBoxAdapter(child: SizedBox()); } return SliverObserveContextToBoxAdapter( child: WaterfallFlowSwipeView(hitType: hitType), onObserve: (context) { if (swipeContext != context) swipeContext = context; }, ); } Widget _buildSeparator(double size) { return SliverToBoxAdapter( child: Container(height: size), ); } } ================================================ FILE: example/lib/features/scene/waterfall_flow_demo/waterfall_flow_swipe_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-06-08 22:03:17 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/features/scene/video_auto_play_list/widgets/video_widget.dart'; import 'package:scrollview_observer_example/features/scene/waterfall_flow_demo/waterfall_flow_type.dart'; class WaterfallFlowSwipeView extends StatefulWidget { final WaterFlowHitType hitType; const WaterfallFlowSwipeView({ Key? key, required this.hitType, }) : super(key: key); @override State createState() => _WaterfallFlowSwipeViewState(); } class _WaterfallFlowSwipeViewState extends State { PageController pageController = PageController(viewportFraction: 0.9); int currentIndex = 0; @override Widget build(BuildContext context) { Widget resultWidget = PageView.builder( controller: pageController, padEnds: false, itemBuilder: (context, index) { final isHit = WaterFlowHitType.swipe == widget.hitType && currentIndex == index; return Padding( padding: const EdgeInsets.only(right: 10), child: Container( color: Colors.blue, child: isHit ? _buildVideo() : const SizedBox.shrink(), ), ); }, itemCount: 4, onPageChanged: (index) { if (currentIndex == index) return; setState(() { currentIndex = index; }); }, ); resultWidget = SizedBox(height: 200, child: resultWidget); return resultWidget; } Widget _buildVideo() { Widget resultWidget = const VideoWidget( url: 'https://www.w3schools.com/html/movie.mp4', ); resultWidget = SizedBox( width: double.infinity, // height: 100, child: resultWidget, ); return resultWidget; } } ================================================ FILE: example/lib/features/scene/waterfall_flow_demo/waterfall_flow_type.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-06-06 22:16:27 */ enum WaterFlowHitType { firstGrid, swipe, secondGrid, } ================================================ FILE: example/lib/features/scene/waterfall_flow_fixed_height_demo/waterfall_flow_fixed_height_page.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-10-25 21:29:37 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer_example/utils/snackbar.dart'; import 'package:waterfall_flow/waterfall_flow.dart'; class WaterfallFlowFixedHeightPage extends StatefulWidget { const WaterfallFlowFixedHeightPage({Key? key}) : super(key: key); @override State createState() => WaterfallFlowFixedHeightPageState(); } class WaterfallFlowFixedHeightPageState extends State { BuildContext? grid1Context; BuildContext? firstChildCtxInViewport; ScrollController scrollController = ScrollController(initialScrollOffset: 0); late SliverObserverController observerController; @override void initState() { super.initState(); observerController = SliverObserverController(controller: scrollController); } @override void dispose() { observerController.controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Waterfall Flow')), body: SliverViewObserver( child: _buildBody(), controller: observerController, autoTriggerObserveTypes: const [ ObserverAutoTriggerObserveType.scrollEnd, ], triggerOnObserveType: ObserverTriggerOnObserveType.directly, // extendedHandleObserve: (context) { // // An extension of the original observation logic. // final _obj = ObserverUtils.findRenderObject(context); // if (_obj is RenderSliverWaterfallFlow) { // return ObserverCore.handleGridObserve( // context: context, // fetchLeadingOffset: () => observeOffset, // ); // } // return null; // }, sliverContexts: () { return [ if (grid1Context != null) grid1Context!, ]; }, onObserveViewport: (result) { firstChildCtxInViewport = result.firstChild.sliverContext; debugPrint( 'current first sliver in viewport - $firstChildCtxInViewport'); }, onObserveAll: (resultMap) { final result = resultMap[firstChildCtxInViewport]; debugPrint('all Observe for current first sliver - $result'); }, ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.airline_stops_outlined), onPressed: () { SnackBarUtil.showSnackBar( context: context, text: 'Jump to item 13', ); observerController.jumpTo( index: 13, isFixedHeight: true, renderSliverType: ObserverRenderSliverType.grid, ); }, ), ); } Widget _buildBody() { return CustomScrollView( controller: scrollController, slivers: [ _buildGridView(childCount: 50), ], ); } Widget _buildGridView({ required int childCount, }) { return SliverWaterfallFlow( gridDelegate: const SliverWaterfallFlowDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 15, crossAxisSpacing: 10, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { if (grid1Context != context) grid1Context = context; return Container( height: 50, color: Colors.primaries[index % Colors.primaries.length], alignment: Alignment.topLeft, child: Text( 'item: $index', style: const TextStyle(fontSize: 20, color: Colors.white), ), ); }, childCount: childCount, ), ); } } ================================================ FILE: example/lib/main.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-05-28 12:32:34 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer_example/common/route/route.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: MyRoute.routerConfig, ); } } ================================================ FILE: example/lib/typedefs.dart ================================================ /// This allows a value of type T or T? /// to be treated as a value of type T?. /// /// We use this so that APIs that have become /// non-nullable can still be used with `!` and `?` /// to support older versions of the API as well. T? ambiguate(T? value) => value; ================================================ FILE: example/lib/utils/keyboard.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-11-08 21:48:11 */ import 'package:flutter/material.dart'; class KeyboardTool { static void dismissKeyboard(BuildContext context) { FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) { FocusManager.instance.primaryFocus?.unfocus(); } } } ================================================ FILE: example/lib/utils/random.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-05-13 12:29:56 */ import 'dart:math'; import 'dart:ui'; class RandomTool { static int genInt({int min = 0, int max = 100}) { var x = Random().nextInt(max) + min; return x.floor(); } static Color color() { final random = Random(); return Color.fromRGBO( random.nextInt(255), random.nextInt(255), random.nextInt(255), 1, ); } } ================================================ FILE: example/lib/utils/snackbar.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-10-29 12:44:03 */ import 'package:flutter/material.dart'; class SnackBarUtil { static showSnackBar({ required BuildContext context, required String text, }) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(text), duration: const Duration(milliseconds: 2000), ), ); } } ================================================ FILE: example/lib/widgets/animation.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-05-29 22:22:07 */ import 'package:flutter/material.dart'; class SlideAnimation extends StatelessWidget { final Curve curve; final double verticalOffset; final double horizontalOffset; final Widget child; final AnimationController controller; const SlideAnimation({ Key? key, this.curve = Curves.ease, required this.controller, double? verticalOffset, double? horizontalOffset, required this.child, }) : verticalOffset = verticalOffset ?? 0.0, horizontalOffset = horizontalOffset ?? 0.0, super(key: key); @override Widget build(BuildContext context) { return AnimationExecutor( controller: controller, builder: (context, animationController) => _slideAnimation(animationController!), ); } Widget _slideAnimation(Animation animation) { Animation offsetAnimation( double offset, Animation animation, ) { return Tween(begin: offset, end: 0.0).animate( CurvedAnimation( parent: animation, curve: Interval(0.0, 1.0, curve: curve), ), ); } return Transform.translate( offset: Offset( horizontalOffset == 0.0 ? 0.0 : offsetAnimation(horizontalOffset, animation).value, verticalOffset == 0.0 ? 0.0 : offsetAnimation(verticalOffset, animation).value, ), child: child, ); } } class FadeInAnimation extends StatelessWidget { final Curve curve; final Widget child; final AnimationController controller; const FadeInAnimation({ Key? key, this.curve = Curves.ease, required this.controller, required this.child, }) : super(key: key); @override Widget build(BuildContext context) { return AnimationExecutor( controller: controller, builder: (context, animationController) => _fadeInAnimation(animationController!), ); } Widget _fadeInAnimation(Animation animation) { final opacityAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: animation, curve: Interval(0.0, 1.0, curve: curve), ), ); return Opacity( opacity: opacityAnimation.value, child: child, ); } } typedef Builder = Widget Function( BuildContext context, AnimationController? animationController, ); class AnimationExecutor extends StatefulWidget { final Builder builder; final AnimationController controller; const AnimationExecutor({ Key? key, required this.builder, required this.controller, }) : super(key: key); @override State createState() => _AnimationExecutorState(); } class _AnimationExecutorState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { return AnimatedBuilder( builder: _buildAnimation, animation: widget.controller, ); } Widget _buildAnimation(BuildContext context, Widget? child) { return widget.builder(context, widget.controller); } } ================================================ FILE: example/lib/widgets/sliver.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-05-29 22:20:12 */ import 'package:flutter/material.dart'; 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, ) { Widget child = builder(context, shrinkOffset, overlapsContent); return SizedBox.expand(child: child); } @override double get maxExtent => maxHeight; @override double get minExtent => minHeight; @override bool shouldRebuild(SliverHeaderDelegate oldDelegate) { return oldDelegate.maxExtent != maxExtent || oldDelegate.minExtent != minExtent; } } ================================================ FILE: example/linux/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" void fl_register_plugins(FlPluginRegistry* registry) { } ================================================ FILE: example/linux/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void fl_register_plugins(FlPluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: example/linux/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: example/macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: example/macos/Flutter/Flutter-Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: example/macos/Flutter/Flutter-Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" ================================================ FILE: example/macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) } ================================================ FILE: example/macos/Podfile ================================================ platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_macos_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) end end ================================================ FILE: example/macos/Runner/AppDelegate.swift ================================================ import Cocoa import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } } ================================================ FILE: example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "app_icon_64.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "app_icon_1024.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: example/macos/Runner/Base.lproj/MainMenu.xib ================================================ ================================================ FILE: example/macos/Runner/Configs/AppInfo.xcconfig ================================================ // Application-level settings for the Runner target. // // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the // future. If not, the values below would default to using the project name when this becomes a // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. PRODUCT_NAME = example // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.lxf.scrollviewobserver.example // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2022 com.lxf.scrollviewobserver. All rights reserved. ================================================ FILE: example/macos/Runner/Configs/Debug.xcconfig ================================================ #include "../../Flutter/Flutter-Debug.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: example/macos/Runner/Configs/Release.xcconfig ================================================ #include "../../Flutter/Flutter-Release.xcconfig" #include "Warnings.xcconfig" ================================================ FILE: example/macos/Runner/Configs/Warnings.xcconfig ================================================ WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings GCC_WARN_UNDECLARED_SELECTOR = YES CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CLANG_WARN_PRAGMA_PACK = YES CLANG_WARN_STRICT_PROTOTYPES = YES CLANG_WARN_COMMA = YES GCC_WARN_STRICT_SELECTOR_MATCH = YES CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES GCC_WARN_SHADOW = YES CLANG_WARN_UNREACHABLE_CODE = YES ================================================ FILE: example/macos/Runner/DebugProfile.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.server com.apple.security.network.client ================================================ FILE: example/macos/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: example/macos/Runner/MainFlutterWindow.swift ================================================ import Cocoa import FlutterMacOS class MainFlutterWindow: NSWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController.init() let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } } ================================================ FILE: example/macos/Runner/Release.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.network.client ================================================ FILE: example/macos/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { isa = PBXAggregateTarget; buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; buildPhases = ( 33CC111E2044C6BF0003C045 /* ShellScript */, ); dependencies = ( ); name = "Flutter Assemble"; productName = FLX; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; E982FC9C597AF2C0BA99DD43 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A30DFC3C9BA76B72CFBF253 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; proxyType = 1; remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 33CC110E2044A8840003C045 /* Bundle Framework */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 4A30DFC3C9BA76B72CFBF253 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5892F63897654C955484C1F9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; 9FDFFB64BFC0B80397556703 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; BD91410C2C0F517239C8D107 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( E982FC9C597AF2C0BA99DD43 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( 33E5194F232828860026EE4D /* AppInfo.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, ); path = Configs; sourceTree = ""; }; 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, D6CB8F0BE283AE47D623916D /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* example.app */, ); name = Products; sourceTree = ""; }; 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, ); name = Resources; path = ..; sourceTree = ""; }; 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, ); path = Flutter; sourceTree = ""; }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, 33CC11242044D66E0003C045 /* Resources */, 33BA886A226E78AF003329D5 /* Configs */, ); path = Runner; sourceTree = ""; }; D6CB8F0BE283AE47D623916D /* Pods */ = { isa = PBXGroup; children = ( BD91410C2C0F517239C8D107 /* Pods-Runner.debug.xcconfig */, 9FDFFB64BFC0B80397556703 /* Pods-Runner.release.xcconfig */, 5892F63897654C955484C1F9 /* Pods-Runner.profile.xcconfig */, ); name = Pods; path = Pods; sourceTree = ""; }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( 4A30DFC3C9BA76B72CFBF253 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 7E62F601627BFAD22181FBF1 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, ); buildRules = ( ); dependencies = ( 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* example.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 33CC111A2044C6BA0003C045 = { CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( Flutter/ephemeral/tripwire, ); outputFileListPaths = ( Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; 7E62F601627BFAD22181FBF1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 33CC10F52044A3C60003C045 /* Base */, ); name = MainMenu.xib; path = Runner; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Profile; }; 338D0CEA231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Profile; }; 338D0CEB231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; }; 33CC10F92044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 33CC10FA2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 33CC10FC2044A3C60003C045 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 33CC10FD2044A3C60003C045 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; 33CC111C2044C6BA0003C045 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 33CC111D2044C6BA0003C045 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10F92044A3C60003C045 /* Debug */, 33CC10FA2044A3C60003C045 /* Release */, 338D0CE9231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC10FC2044A3C60003C045 /* Debug */, 33CC10FD2044A3C60003C045 /* Release */, 338D0CEA231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { isa = XCConfigurationList; buildConfigurations = ( 33CC111C2044C6BA0003C045 /* Debug */, 33CC111D2044C6BA0003C045 /* Release */, 338D0CEB231458BD00FA5F75 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } ================================================ FILE: example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: example/macos/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: example/pubspec.yaml ================================================ name: scrollview_observer_example description: A new Flutter project. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.0+1 environment: sdk: ">=2.19.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, # dependencies can be manually updated by changing the version numbers below to # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 video_player: ^2.4.2 waterfall_flow: ^3.0.2 loading_more_list: ^5.0.3 easy_refresh: ^3.3.2+4 visibility_detector: ^0.4.0+2 flutter_sticky_header: ^0.8.0 carousel_slider: ^5.0.0 go_router: ^16.0.0 get: any getx_helper: ^0.0.1 scrollview_observer: path: ../ dependency_overrides: get: git: url: https://github.com/LinXunFeng/getx_fix.git ref: 4.6.6_fix dev_dependencies: flutter_test: sdk: flutter # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^1.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: example/test/widget_test.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-05-28 12:32:34 */ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility that Flutter provides. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:scrollview_observer_example/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(const MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); } ================================================ FILE: example/web/index.html ================================================ example ================================================ FILE: example/web/manifest.json ================================================ { "name": "example", "short_name": "example", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: example/windows/.gitignore ================================================ flutter/ephemeral/ # Visual Studio user-specific files. *.suo *.user *.userosscache *.sln.docstates # Visual Studio build-related files. x64/ x86/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ ================================================ FILE: example/windows/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) project(example LANGUAGES CXX) set(BINARY_NAME "example") cmake_policy(SET CMP0063 NEW) set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Configure build options. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" CACHE STRING "" FORCE) else() if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Flutter build mode" FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Profile" "Release") endif() endif() set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") # Flutter library and tool build rules. add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build add_subdirectory("runner") # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) # === Installation === # Support files are copied into place next to the executable, so that it can # run in place. This is done instead of making a separate bundle (as on Linux) # so that building and running from within Visual Studio will work. set(BUILD_BUNDLE_DIR "$") # Make the "install" step default, as it's required to run. set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) endif() set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" COMPONENT Runtime) install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif() # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") install(CODE " file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") " COMPONENT Runtime) install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) # Install the AOT library on non-Debug builds only. install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" CONFIGURATIONS Profile;Release COMPONENT Runtime) ================================================ FILE: example/windows/flutter/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") # Configuration provided via flutter tool. include(${EPHEMERAL_DIR}/generated_config.cmake) # TODO: Move the rest of this into files in ephemeral. See # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") # Published to parent scope for install step. set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) list(APPEND FLUTTER_LIBRARY_HEADERS "flutter_export.h" "flutter_windows.h" "flutter_messenger.h" "flutter_plugin_registrar.h" "flutter_texture_registrar.h" ) list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") add_library(flutter INTERFACE) target_include_directories(flutter INTERFACE "${EPHEMERAL_DIR}" ) target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") add_dependencies(flutter flutter_assemble) # === Wrapper === list(APPEND CPP_WRAPPER_SOURCES_CORE "core_implementations.cc" "standard_codec.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_PLUGIN "plugin_registrar.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") # Wrapper sources needed for a plugin. add_library(flutter_wrapper_plugin STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ) apply_standard_settings(flutter_wrapper_plugin) set_target_properties(flutter_wrapper_plugin PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(flutter_wrapper_plugin PROPERTIES CXX_VISIBILITY_PRESET hidden) target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) target_include_directories(flutter_wrapper_plugin PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_plugin flutter_assemble) # Wrapper sources needed for the runner. add_library(flutter_wrapper_app STATIC ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_APP} ) apply_standard_settings(flutter_wrapper_app) target_link_libraries(flutter_wrapper_app PUBLIC flutter) target_include_directories(flutter_wrapper_app PUBLIC "${WRAPPER_ROOT}/include" ) add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" windows-x64 $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} ) ================================================ FILE: example/windows/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" void RegisterPlugins(flutter::PluginRegistry* registry) { } ================================================ FILE: example/windows/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void RegisterPlugins(flutter::PluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: example/windows/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: example/windows/runner/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) apply_standard_settings(${BINARY_NAME}) target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") add_dependencies(${BINARY_NAME} flutter_assemble) ================================================ FILE: example/windows/runner/Runner.rc ================================================ // Microsoft Visual C++ generated resource script. // #pragma code_page(65001) #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (United States) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP_ICON ICON "resources\\app_icon.ico" ///////////////////////////////////////////////////////////////////////////// // // Version // #ifdef FLUTTER_BUILD_NUMBER #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER #else #define VERSION_AS_NUMBER 1,0,0 #endif #ifdef FLUTTER_BUILD_NAME #define VERSION_AS_STRING #FLUTTER_BUILD_NAME #else #define VERSION_AS_STRING "1.0.0" #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER PRODUCTVERSION VERSION_AS_NUMBER FILEFLAGSMASK VS_FFI_FILEFLAGSMASK #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG #else FILEFLAGS 0x0L #endif FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" VALUE "FileDescription", "example" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "example" "\0" VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" VALUE "OriginalFilename", "example.exe" "\0" VALUE "ProductName", "example" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED ================================================ FILE: example/windows/runner/flutter_window.cpp ================================================ #include "flutter_window.h" #include #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} FlutterWindow::~FlutterWindow() {} bool FlutterWindow::OnCreate() { if (!Win32Window::OnCreate()) { return false; } RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface // creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. if (!flutter_controller_->engine() || !flutter_controller_->view()) { return false; } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { flutter_controller_ = nullptr; } Win32Window::OnDestroy(); } LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, lparam); if (result) { return *result; } } switch (message) { case WM_FONTCHANGE: flutter_controller_->engine()->ReloadSystemFonts(); break; } return Win32Window::MessageHandler(hwnd, message, wparam, lparam); } ================================================ FILE: example/windows/runner/flutter_window.h ================================================ #ifndef RUNNER_FLUTTER_WINDOW_H_ #define RUNNER_FLUTTER_WINDOW_H_ #include #include #include #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: // Creates a new FlutterWindow hosting a Flutter view running |project|. explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: // Win32Window: bool OnCreate() override; void OnDestroy() override; LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; private: // The project to run. flutter::DartProject project_; // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ ================================================ FILE: example/windows/runner/main.cpp ================================================ #include #include #include #include "flutter_window.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); } // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); std::vector command_line_arguments = GetCommandLineArguments(); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); if (!window.CreateAndShow(L"example", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } ::CoUninitialize(); return EXIT_SUCCESS; } ================================================ FILE: example/windows/runner/resource.h ================================================ //{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // #define IDI_APP_ICON 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif ================================================ FILE: example/windows/runner/runner.exe.manifest ================================================ PerMonitorV2 ================================================ FILE: example/windows/runner/utils.cpp ================================================ #include "utils.h" #include #include #include #include #include void CreateAndAttachConsole() { if (::AllocConsole()) { FILE *unused; if (freopen_s(&unused, "CONOUT$", "w", stdout)) { _dup2(_fileno(stdout), 1); } if (freopen_s(&unused, "CONOUT$", "w", stderr)) { _dup2(_fileno(stdout), 2); } std::ios::sync_with_stdio(); FlutterDesktopResyncOutputStreams(); } } std::vector GetCommandLineArguments() { // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. int argc; wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); if (argv == nullptr) { return std::vector(); } std::vector command_line_arguments; // Skip the first argument as it's the binary name. for (int i = 1; i < argc; i++) { command_line_arguments.push_back(Utf8FromUtf16(argv[i])); } ::LocalFree(argv); return command_line_arguments; } std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr); if (target_length == 0) { return std::string(); } std::string utf8_string; utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } return utf8_string; } ================================================ FILE: example/windows/runner/utils.h ================================================ #ifndef RUNNER_UTILS_H_ #define RUNNER_UTILS_H_ #include #include // Creates a console for the process, and redirects stdout and stderr to // it for both the runner and the Flutter library. void CreateAndAttachConsole(); // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string // encoded in UTF-8. Returns an empty std::string on failure. std::string Utf8FromUtf16(const wchar_t* utf16_string); // Gets the command line arguments passed in as a std::vector, // encoded in UTF-8. Returns an empty std::vector on failure. std::vector GetCommandLineArguments(); #endif // RUNNER_UTILS_H_ ================================================ FILE: example/windows/runner/win32_window.cpp ================================================ #include "win32_window.h" #include #include "resource.h" namespace { constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in // scale factor int Scale(int source, double scale_factor) { return static_cast(source * scale_factor); } // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. // This API is only needed for PerMonitor V1 awareness mode. void EnableFullDpiSupportIfAvailable(HWND hwnd) { HMODULE user32_module = LoadLibraryA("User32.dll"); if (!user32_module) { return; } auto enable_non_client_dpi_scaling = reinterpret_cast( GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); FreeLibrary(user32_module); } } } // namespace // Manages the Win32Window's window class registration. class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; // Returns the singleton registar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); } return instance_; } // Returns the name of the window class, registering the class if it hasn't // previously been registered. const wchar_t* GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); private: WindowClassRegistrar() = default; static WindowClassRegistrar* instance_; bool class_registered_ = false; }; WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; const wchar_t* WindowClassRegistrar::GetWindowClass() { if (!class_registered_) { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; window_class.style = CS_HREDRAW | CS_VREDRAW; window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; RegisterClass(&window_class); class_registered_ = true; } return kWindowClassName; } void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } Win32Window::Win32Window() { ++g_active_window_count; } Win32Window::~Win32Window() { --g_active_window_count; Destroy(); } bool Win32Window::CreateAndShow(const std::wstring& title, const Point& origin, const Size& size) { Destroy(); const wchar_t* window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), static_cast(origin.y)}; HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); if (!window) { return false; } return OnCreate(); } // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { if (message == WM_NCCREATE) { auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; } else if (Win32Window* that = GetThisFromHandle(window)) { return that->MessageHandler(window, message, wparam, lparam); } return DefWindowProc(window, message, wparam, lparam); } LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { switch (message) { case WM_DESTROY: window_handle_ = nullptr; Destroy(); if (quit_on_close_) { PostQuitMessage(0); } return 0; case WM_DPICHANGED: { auto newRectSize = reinterpret_cast(lparam); LONG newWidth = newRectSize->right - newRectSize->left; LONG newHeight = newRectSize->bottom - newRectSize->top; SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, newHeight, SWP_NOZORDER | SWP_NOACTIVATE); return 0; } case WM_SIZE: { RECT rect = GetClientArea(); if (child_content_ != nullptr) { // Size and position the child window. MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); } return 0; } case WM_ACTIVATE: if (child_content_ != nullptr) { SetFocus(child_content_); } return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } void Win32Window::Destroy() { OnDestroy(); if (window_handle_) { DestroyWindow(window_handle_); window_handle_ = nullptr; } if (g_active_window_count == 0) { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } void Win32Window::SetChildContent(HWND content) { child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); MoveWindow(content, frame.left, frame.top, frame.right - frame.left, frame.bottom - frame.top, true); SetFocus(child_content_); } RECT Win32Window::GetClientArea() { RECT frame; GetClientRect(window_handle_, &frame); return frame; } HWND Win32Window::GetHandle() { return window_handle_; } void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } bool Win32Window::OnCreate() { // No-op; provided for subclasses. return true; } void Win32Window::OnDestroy() { // No-op; provided for subclasses. } ================================================ FILE: example/windows/runner/win32_window.h ================================================ #ifndef RUNNER_WIN32_WINDOW_H_ #define RUNNER_WIN32_WINDOW_H_ #include #include #include #include // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling class Win32Window { public: struct Point { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; struct Size { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) : width(width), height(height) {} }; Win32Window(); virtual ~Win32Window(); // Creates and shows a win32 window with |title| and position and size using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a // consistent size to will treat the width height passed in to this function // as logical pixels and scale to appropriate for the default monitor. Returns // true if the window was created successfully. bool CreateAndShow(const std::wstring& title, const Point& origin, const Size& size); // Release OS resources associated with window. void Destroy(); // Inserts |content| into the window tree. void SetChildContent(HWND content); // Returns the backing Window handle to enable clients to set icon and other // window properties. Returns nullptr if the window has been destroyed. HWND GetHandle(); // If true, closing this window will quit the application. void SetQuitOnClose(bool quit_on_close); // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. virtual LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related // setup. Subclasses should return false if setup fails. virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically // responsponds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; bool quit_on_close_ = false; // window handle for top level window. HWND window_handle_ = nullptr; // window handle for hosted content. HWND child_content_ = nullptr; }; #endif // RUNNER_WIN32_WINDOW_H_ ================================================ FILE: lib/scrollview_observer.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ library scrollview_observer; export 'src/notification.dart'; export 'src/common/models/observe_model.dart'; export 'src/common/models/observe_displaying_child_model.dart'; export 'src/common/models/observe_displaying_child_model_mixin.dart'; export 'src/common/models/observer_index_position_model.dart'; export 'src/common/observer_typedef.dart'; export 'src/common/observer_notification_result.dart'; export 'src/listview/list_observer_view.dart'; export 'src/listview/list_observer_controller.dart'; export 'src/listview/models/listview_observe_model.dart'; export 'src/listview/models/listview_observe_displaying_child_model.dart'; export 'src/listview/list_observer_notification_result.dart'; export 'src/gridview/grid_observer_view.dart'; export 'src/gridview/grid_observer_controller.dart'; export 'src/gridview/models/gridview_observe_model.dart'; export 'src/gridview/models/gridview_observe_displaying_child_model.dart'; export 'src/gridview/grid_observer_notification_result.dart'; export 'src/sliver/sliver_observer_view.dart'; export 'src/sliver/sliver_observer_controller.dart'; export 'src/sliver/models/sliver_viewport_observe_model.dart'; export 'src/sliver/models/sliver_viewport_observe_displaying_child_model.dart'; export 'src/sliver/sliver_observer_notification_result.dart'; export 'src/utils/observer_utils.dart'; export 'src/observer_core.dart'; ================================================ FILE: lib/src/common/models/observe_displaying_child_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/rendering.dart'; abstract class ObserveDisplayingChildModel { /// The target sliverList. RenderSliver sliver; /// The viewport of sliver. RenderViewportBase viewport; /// The index of child widget. int index; /// The renderObject [RenderBox] of child widget. RenderBox renderObject; ObserveDisplayingChildModel({ required this.sliver, required this.viewport, required this.index, required this.renderObject, }); } ================================================ FILE: lib/src/common/models/observe_displaying_child_model_mixin.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/rendering.dart'; import 'observe_displaying_child_model.dart'; mixin ObserveDisplayingChildModelMixin on ObserveDisplayingChildModel { /// The axis of sliver. Axis get axis => sliver.constraints.axis; /// The size of child widget. Size get size => renderObject.size; /// The size of child widget on the main axis. double get mainAxisSize => axis == Axis.vertical ? size.height : size.width; /// The scroll offset of sliver double get scrollOffset => sliver.constraints.scrollOffset; /// The overlap of sliver double get overlap => sliver.constraints.overlap; /// The amount of currently visible visual space to the [sliver]. double get paintExtent => sliver.geometry?.paintExtent ?? 0; /// The precedingScrollExtent of sliver /// /// Note: Before Flutter 3.22.0, this value may be inaccurate in some scenarios. /// Related PR: https://github.com/flutter/flutter/pull/143661 double get precedingScrollExtent => sliver.constraints.precedingScrollExtent; /// The layout offset of child widget. double get layoutOffset { final parentData = renderObject.parentData; if (parentData is! SliverLogicalParentData) return 0; return parentData.layoutOffset ?? 0; } /// Whether the [pixels] property of viewport is available. bool get viewportHasPixels => viewport.offset.hasPixels; /// The number of pixels the viewport can display in the main axis. double get viewportMainAxisExtent => sliver.constraints.viewportMainAxisExtent; /// The number of pixels to offset the children in the opposite of the axis /// direction. double get viewportPixels => viewport.offset.hasPixels ? viewport.offset.pixels : 0; /// The margin from the top of the child widget to the viewport. double get leadingMarginToViewport => layoutOffset + precedingScrollExtent - viewportPixels; /// The margin from the bottom of the child widget to the viewport. double get trailingMarginToViewport => viewportMainAxisExtent - leadingMarginToViewport - mainAxisSize; /// The visible size of the child widget in the main axis. double get visibleMainAxisSize { if (paintExtent == 0) return 0; double _visibleMainAxisSize = mainAxisSize; final currentChildLayoutOffset = layoutOffset; final scrollOffset = sliver.constraints.scrollOffset; final leadingScrollViewOffset = scrollOffset + overlap; if (leadingScrollViewOffset > currentChildLayoutOffset) { // The child widget is blocked by the leading direction side of viewport. _visibleMainAxisSize = mainAxisSize - (leadingScrollViewOffset - currentChildLayoutOffset); } else if (scrollOffset + paintExtent > currentChildLayoutOffset + mainAxisSize) { // The child widget is being fully displayed. _visibleMainAxisSize = mainAxisSize; } else { // The child widget is blocked by the trailing direction side of viewport. _visibleMainAxisSize = scrollOffset + paintExtent - currentChildLayoutOffset; } _visibleMainAxisSize = _visibleMainAxisSize.clamp(0, mainAxisSize); return _visibleMainAxisSize; } /// The visible fraction of the child widget on the corresponding sliver. double get visibleFraction { if (paintExtent == 0) return 0; return (visibleMainAxisSize / paintExtent).clamp(0, 1); } /// The display percentage of the current widget double get displayPercentage { if (mainAxisSize == 0) return 0; return (visibleMainAxisSize / mainAxisSize).clamp(0, 1); } } ================================================ FILE: lib/src/common/models/observe_find_child_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-09-26 23:05:33 */ import 'package:flutter/rendering.dart'; /// [ObserveFindChildModel] is used to pass data internally. class ObserveFindChildModel { ObserveFindChildModel({ required this.sliver, required this.viewport, required this.index, required this.renderObject, }); /// The target sliverList. RenderSliver sliver; /// The viewport of sliver. RenderViewportBase viewport; /// The index of child widget. int index; /// The renderObject [RenderIndexedSemantics] of child widget. RenderIndexedSemantics renderObject; } ================================================ FILE: lib/src/common/models/observe_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/rendering.dart'; import 'observe_displaying_child_model.dart'; abstract class ObserveModel { /// Whether this sliver should be painted. bool visible; /// The target sliver. RenderSliver sliver; /// The viewport of sliver. RenderViewportBase viewport; /// Stores model list for children widgets those are displaying. List innerDisplayingChildModelList; /// Stores model map for children widgets those are displaying. Map innerDisplayingChildModelMap; /// Stores index list for children widgets those are displaying. List get displayingChildIndexList => innerDisplayingChildModelList.map((e) => e.index).toList(); /// The axis of sliver. Axis get axis => sliver.constraints.axis; /// The scroll offset of sliver. double get scrollOffset => sliver.constraints.scrollOffset; ObserveModel({ required this.visible, required this.sliver, required this.viewport, required this.innerDisplayingChildModelList, required this.innerDisplayingChildModelMap, }); } ================================================ FILE: lib/src/common/models/observe_scroll_child_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-07-20 00:32:40 */ class ObserveScrollChildModel { /// The size of child widget. double size; /// The layout offset of child widget. double layoutOffset; ObserveScrollChildModel({ required this.size, required this.layoutOffset, }); } ================================================ FILE: lib/src/common/models/observe_scroll_to_index_result_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-10-25 21:29:37 */ import 'package:flutter/rendering.dart'; class ObserveScrollToIndexFixedHeightResultModel { /// The size of item on the main axis. double childMainAxisSize; /// The separator size between items on the main axis. double itemSeparatorHeight; /// The number of rows for the target item. int indexOfLine; /// The offset of the target child widget on the main axis. double targetChildLayoutOffset; ObserveScrollToIndexFixedHeightResultModel({ required this.childMainAxisSize, required this.itemSeparatorHeight, required this.indexOfLine, required this.targetChildLayoutOffset, }); } class ObservePrepareScrollToIndexModel { /// The scroll distance that has been consumed by all [RenderSliver]s that /// came before this [RenderSliver]. double precedingScrollExtent; /// The target safety layout offset for scrolling to index. double calculateTargetLayoutOffset; /// The offset of the target child widget on the main axis. double targetChildLayoutOffset; ObservePrepareScrollToIndexModel({ required this.calculateTargetLayoutOffset, required this.precedingScrollExtent, required this.targetChildLayoutOffset, }); } ================================================ FILE: lib/src/common/models/observer_handle_contexts_result_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-08-12 16:01:29 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/src/common/models/observe_model.dart'; class ObserverHandleContextsResultModel { /// Observation result for first sliver. /// Corresponding to [onObserve] in [ObserverWidget]. final M? changeResultModel; /// Observation result map. /// Corresponding to [onObserveAll] in [ObserverWidget]. final Map changeResultMap; ObserverHandleContextsResultModel({ this.changeResultModel, this.changeResultMap = const {}, }); } ================================================ FILE: lib/src/common/models/observer_index_position_model.dart ================================================ import 'package:flutter/material.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; class ObserverIndexPositionModel { ObserverIndexPositionModel({ required this.index, this.sliverContext, this.isFixedHeight = false, this.alignment = 0, this.offset, this.padding = EdgeInsets.zero, }); /// The index position of the scrollView. int index; /// The target sliver [BuildContext]. BuildContext? sliverContext; /// If the height of the child widget and the height of the separator are /// fixed, please pass [true] to this property. bool isFixedHeight; /// The [alignment] specifies the desired position for the leading edge of the /// child widget. /// /// It must be a value in the range [0.0, 1.0]. double alignment; /// Use this property when locating position needs an offset. ObserverLocateIndexOffsetCallback? offset; /// This value is required when the scrollView is wrapped in the /// [SliverPadding]. /// /// For example: /// 1. ListView.separated(padding: _padding, ...) /// 2. GridView.builder(padding: _padding, ...) EdgeInsets padding; } ================================================ FILE: lib/src/common/observer_controller.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/models/observe_find_child_model.dart'; import 'package:scrollview_observer/src/common/models/observe_scroll_to_index_result_model.dart'; import 'package:scrollview_observer/src/common/models/observer_handle_contexts_result_model.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; import 'package:scrollview_observer/src/utils/src/log.dart'; import 'models/observe_scroll_child_model.dart'; class ObserverController { ObserverController({this.controller}); /// Target scroll controller. ScrollController? controller; /// The map which stores the offset of child in the sliver Map> indexOffsetMap = {}; /// Target sliver [BuildContext] List sliverContexts = []; /// In the case where the observation is triggered during scrolling, the /// minimum amount of time to wait for firing observe callback. /// /// Defaults to Duration.zero. Duration observeIntervalForScrolling = Duration.zero; /// Whether to forbid the onObserve callback and onObserveAll callback. bool isForbidObserveCallback = false; /// A flag used to ignore unnecessary calculations during scrolling. bool innerIsHandlingScroll = false; /// The callback to call [ObserverWidget]'s [_handleContexts] method. Function()? innerNeedOnceObserveCallBack; /// The callback to call [ObserverWidget]'s [_setupSliverController] method. Function()? innerReattachCallBack; /// Reset all data void innerReset() { indexOffsetMap = {}; innerIsHandlingScroll = false; } /// Get the target sliver [BuildContext] BuildContext? fetchSliverContext({BuildContext? sliverContext}) { BuildContext? _sliverContext = sliverContext; if (_sliverContext == null && sliverContexts.isNotEmpty) { _sliverContext = sliverContexts.first; } return _sliverContext; } /// Get the latest target sliver [BuildContext] and reset some of the old data. void reattach() { if (innerReattachCallBack == null) return; ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { innerReattachCallBack!(); }); } } mixin ObserverControllerForNotification< M extends ObserveModel, R extends ObserverHandleContextsResultModel, S extends CommonOnceObserveNotificationResult> on ObserverController { /// A completer for dispatch once observation Completer? innerDispatchOnceObserveCompleter; /// Dispatch a observation notification Future innerDispatchOnceObserve({ BuildContext? sliverContext, required Notification notification, }) { Completer completer = Completer(); innerDispatchOnceObserveCompleter = completer; BuildContext? _sliverContext = fetchSliverContext( sliverContext: sliverContext, ); notification.dispatch(_sliverContext); return completer.future; } /// Complete the observation notification innerHandleDispatchOnceObserveComplete({ required R? resultModel, }) { final completer = innerDispatchOnceObserveCompleter; if (completer == null) return; if (!completer.isCompleted) { final isSuccess = resultModel != null; final resultType = isSuccess ? ObserverWidgetObserveResultType.success : ObserverWidgetObserveResultType.interrupted; final result = innerCreateOnceObserveNotificationResult( resultType: resultType, resultModel: resultModel, ); completer.complete(result); } innerDispatchOnceObserveCompleter = null; } /// Create a observation notification result. S innerCreateOnceObserveNotificationResult({ required ObserverWidgetObserveResultType resultType, required R? resultModel, }) { // The class being mixed in will implement it and will not return null. throw UnimplementedError(); } } mixin ObserverControllerForInfo on ObserverController { /// Find out the current first child in sliver RenderIndexedSemantics? findCurrentFirstChild( RenderSliverMultiBoxAdaptor obj, ) { RenderIndexedSemantics? child; final firstChild = obj.firstChild; if (firstChild == null) return null; if (firstChild is RenderIndexedSemantics) { child = firstChild; } else { final nextChild = obj.childAfter(firstChild); if (nextChild is RenderIndexedSemantics) { child = nextChild; } } return child; } /// Find out the next child in sliver RenderIndexedSemantics? findNextChild({ required RenderSliverMultiBoxAdaptor obj, RenderBox? currentChild, }) { RenderIndexedSemantics? child; if (currentChild == null) return null; var nextChild = obj.childAfter(currentChild); if (nextChild == null) return null; if (nextChild is RenderIndexedSemantics) { child = nextChild; } else { nextChild = obj.childAfter(nextChild); if (nextChild is RenderIndexedSemantics) { child = nextChild; } } return child; } /// Find out the current last child in sliver RenderIndexedSemantics? findCurrentLastChild( RenderSliverMultiBoxAdaptor obj) { RenderIndexedSemantics? child; final lastChild = obj.lastChild; if (lastChild == null) return null; if (lastChild is RenderIndexedSemantics) { child = lastChild; } else { final previousChild = obj.childBefore(lastChild); if (previousChild is RenderIndexedSemantics) { child = previousChild; } } return child; } /// Find out the child widget info for specified index in sliver. ObserveFindChildModel? findChildInfo({ required int index, BuildContext? sliverContext, }) { final ctx = fetchSliverContext(sliverContext: sliverContext); var obj = ObserverUtils.findRenderObject(ctx); if (obj is! RenderSliverMultiBoxAdaptor) return null; final viewport = ObserverUtils.findViewport(obj); if (viewport == null) return null; var targetChild = findCurrentFirstChild(obj); if (targetChild == null) return null; while (targetChild != null && (targetChild.index != index)) { targetChild = findNextChild(obj: obj, currentChild: targetChild); } if (targetChild == null) return null; return ObserveFindChildModel( sliver: obj, viewport: viewport, index: targetChild.index, renderObject: targetChild, ); } /// Find out the first child widget info in sliver. ObserveFindChildModel? findCurrentFirstChildInfo({ BuildContext? sliverContext, }) { final ctx = fetchSliverContext(sliverContext: sliverContext); var obj = ObserverUtils.findRenderObject(ctx); if (obj == null || obj is! RenderSliverMultiBoxAdaptor) return null; final targetChild = findCurrentFirstChild(obj); if (targetChild == null) return null; final index = targetChild.index; return findChildInfo(index: index, sliverContext: sliverContext); } /// Find out the viewport RenderViewportBase? _findViewport(RenderSliverMultiBoxAdaptor obj) { return ObserverUtils.findViewport(obj); } /// Getting [maxScrollExtent] of viewport double viewportMaxScrollExtent(RenderViewportBase viewport) { final offset = viewport.offset; if (offset is! ScrollPosition) { return 0; } return offset.maxScrollExtent; } /// Getting the extreme scroll extent of viewport. /// The [maxScrollExtent] will be returned when growthDirection is forward. /// The [minScrollExtent] will be returned when growthDirection is reverse. double viewportExtremeScrollExtent({ required RenderViewportBase viewport, required RenderSliverMultiBoxAdaptor obj, }) { final offset = viewport.offset; if (offset is! ScrollPosition) { return 0; } return obj.isForwardGrowthDirection ? offset.maxScrollExtent : offset.minScrollExtent; } } mixin ObserverControllerForScroll on ObserverControllerForInfo { static const Duration _findingDuration = Duration(milliseconds: 1); static const Curve _findingCurve = Curves.ease; /// Whether to cache the offset when jump to a specified index position. /// Defaults to true. bool cacheJumpIndexOffset = true; /// The initial index position of the scrollView. /// /// Defaults to zero. int get initialIndex => initialIndexModel.index; set initialIndex(int index) { initialIndexModel = ObserverIndexPositionModel(index: index); } /// The initial index position model of the scrollView. /// /// Defaults to ObserverIndexPositionModel(index: 0, sliverContext: null). ObserverIndexPositionModel initialIndexModel = ObserverIndexPositionModel( index: 0, ); /// The block to return [ObserverIndexPositionModel] which to init index /// position. ObserverIndexPositionModel Function()? initialIndexModelBlock; /// Clear the offset cache that jumping to a specified index location. @Deprecated( 'It will be removed in version 2, please use [clearScrollIndexCache] instead') clearIndexOffsetCache(BuildContext? sliverContext) { clearScrollIndexCache(sliverContext: sliverContext); } /// Clear the offset cache that jumping to a specified index location. clearScrollIndexCache({BuildContext? sliverContext}) { final ctx = fetchSliverContext(sliverContext: sliverContext); if (ctx == null) return; indexOffsetMap[ctx]?.clear(); } /// Init index position for scrollView. innerInitialIndexPosition() { final model = initialIndexModelBlock?.call() ?? initialIndexModel; if (model.sliverContext == null && model.index <= 0) return; innerJumpTo( index: model.index, sliverContext: model.sliverContext, isFixedHeight: model.isFixedHeight, alignment: model.alignment, padding: model.padding, offset: model.offset, ); } /// Jump to the specified index position without animation. /// /// If the height of the child widget and the height of the separator are /// fixed, please pass the [isFixedHeight] parameter and the /// [renderSliverType] parameter. /// /// If you do not pass the [isFixedHeight] parameter, the package will /// automatically gradually scroll around the target location before /// locating, which will produce an animation. /// /// The [renderSliverType] parameter is used to specify the type of sliver. /// If you do not pass the [renderSliverType] parameter, the sliding position /// will be calculated based on the actual type of obj, and there may be /// deviations in the calculation of elements for third-party libraries. /// /// The [alignment] specifies the desired position for the leading edge of the /// child widget. It must be a value in the range [0.0, 1.0]. Future innerJumpTo({ required int index, BuildContext? sliverContext, bool isFixedHeight = false, double alignment = 0, EdgeInsets padding = EdgeInsets.zero, ObserverLocateIndexOffsetCallback? offset, ObserverRenderSliverType? renderSliverType, ObserverOnPrepareScrollToIndex? onPrepareScrollToIndex, }) { Completer completer = Completer(); _scrollToIndex( completer: completer, index: index, isFixedHeight: isFixedHeight, alignment: alignment, padding: padding, sliverContext: sliverContext, offset: offset, renderSliverType: renderSliverType, onPrepareScrollToIndex: onPrepareScrollToIndex, ); return completer.future; } /// Jump to the specified index position with animation. /// /// If the height of the child widget and the height of the separator are /// fixed, please pass the [isFixedHeight] parameter and the /// [renderSliverType] parameter. /// /// The [renderSliverType] parameter is used to specify the type of sliver. /// If you do not pass the [renderSliverType] parameter, the sliding position /// will be calculated based on the actual type of obj, and there may be /// deviations in the calculation of elements for third-party libraries. /// /// The [alignment] specifies the desired position for the leading edge of the /// child widget. It must be a value in the range [0.0, 1.0]. Future innerAnimateTo({ required int index, required Duration duration, required Curve curve, EdgeInsets padding = EdgeInsets.zero, BuildContext? sliverContext, bool isFixedHeight = false, double alignment = 0, ObserverLocateIndexOffsetCallback? offset, ObserverRenderSliverType? renderSliverType, ObserverOnPrepareScrollToIndex? onPrepareScrollToIndex, }) { Completer completer = Completer(); _scrollToIndex( completer: completer, index: index, isFixedHeight: isFixedHeight, alignment: alignment, padding: padding, sliverContext: sliverContext, duration: duration, curve: curve, offset: offset, renderSliverType: renderSliverType, onPrepareScrollToIndex: onPrepareScrollToIndex, ); return completer.future; } _scrollToIndex({ required Completer completer, required int index, required bool isFixedHeight, required double alignment, required EdgeInsets padding, required BuildContext? sliverContext, Duration? duration, Curve? curve, required ObserverLocateIndexOffsetCallback? offset, required ObserverRenderSliverType? renderSliverType, required ObserverOnPrepareScrollToIndex? onPrepareScrollToIndex, }) async { assert(alignment.clamp(0, 1) == alignment, 'The [alignment] is expected to be a value in the range [0.0, 1.0]'); assert(controller != null); var _controller = controller; final ctx = fetchSliverContext(sliverContext: sliverContext); if (ctx == null) { _handleScrollInterruption(context: ctx, completer: completer); return; } if (_controller == null || !_controller.hasClients) { _handleScrollInterruption(context: ctx, completer: completer); return; } var obj = ObserverUtils.findRenderObject(ctx); if (obj is! RenderSliverMultiBoxAdaptor) { _handleScrollInterruption(context: ctx, completer: completer); return; } final viewport = _findViewport(obj); if (viewport == null) { _handleScrollInterruption(context: ctx, completer: completer); return; } // Start executing scrolling task. _handleScrollStart(context: ctx); bool isAnimateTo = (duration != null) && (curve != null); // Before the next sliver is shown, it may have an incorrect value for // precedingScrollExtent, so we need to scroll around to get // precedingScrollExtent correctly. final objVisible = obj.geometry?.visible ?? false; if (!objVisible && viewport.offset.hasPixels) { final extremeScrollExtent = viewportExtremeScrollExtent( viewport: viewport, obj: obj, ); final maxScrollExtent = extremeScrollExtent.rectify(obj); // If the target sliver does not paint any child because it is too far // away, we need to let the ScrollView scroll near it first. // https://github.com/LinXunFeng/flutter_scrollview_observer/issues/45 if (obj.firstChild == null) { final constraints = obj.constraints; final precedingScrollExtent = constraints.precedingScrollExtent; double paintScrollExtent = precedingScrollExtent + (obj.geometry?.maxPaintExtent ?? 0); double targetScrollExtent = precedingScrollExtent; final pixels = _controller.position.pixels.rectify(obj); if (pixels > paintScrollExtent) { targetScrollExtent = paintScrollExtent; } if (targetScrollExtent > maxScrollExtent) { targetScrollExtent = maxScrollExtent; } await _controller.animateTo( targetScrollExtent.rectify(obj), duration: _findingDuration, curve: _findingCurve, ); await WidgetsBinding.instance.endOfFrame; } else { final precedingScrollExtent = obj.constraints.precedingScrollExtent; final viewportOffset = viewport.offset.pixels.rectify(obj); final isHorizontal = obj.constraints.axis == Axis.horizontal; final viewportSize = isHorizontal ? viewport.size.width : viewport.size.height; final viewportBoundaryExtent = viewportSize * 0.5 + (viewport.cacheExtent ?? 0); if (precedingScrollExtent > (viewportOffset + viewportBoundaryExtent)) { double targetOffset = precedingScrollExtent - viewportBoundaryExtent; if (targetOffset > maxScrollExtent) targetOffset = maxScrollExtent; await _controller.animateTo( targetOffset.rectify(obj), duration: _findingDuration, curve: _findingCurve, ); await WidgetsBinding.instance.endOfFrame; } } } var targetScrollChildModel = indexOffsetMap[ctx]?[index]; // There is a cache offset, scroll to the offset directly. if (targetScrollChildModel != null) { _handleScrollDecision(context: ctx); var calcResult = _calculateTargetLayoutOffset( obj: obj, childLayoutOffset: targetScrollChildModel.layoutOffset, childSize: targetScrollChildModel.size, alignment: alignment, padding: padding, offset: offset, ); await _scrollTo( isAnimateTo: isAnimateTo, duration: duration, curve: curve, controller: _controller, calcResult: calcResult, onPrepareScrollToIndex: onPrepareScrollToIndex, ); _handleScrollEnd(context: ctx, completer: completer); return; } // Because it is fixed height, the offset can be directly calculated for // locating. if (isFixedHeight) { _handleScrollToIndexForFixedHeight( completer: completer, ctx: ctx, obj: obj, index: index, alignment: alignment, padding: padding, duration: duration, curve: curve, offset: offset, renderSliverType: renderSliverType, onPrepareScrollToIndex: onPrepareScrollToIndex, ); return; } // Find the index of the first [RenderIndexedSemantics] child in viewport var firstChildIndex = 0; var lastChildIndex = 0; final firstChild = findCurrentFirstChild(obj); final lastChild = findCurrentLastChild(obj); if (firstChild == null || lastChild == null) { _handleScrollInterruption(context: ctx, completer: completer); return; } firstChildIndex = firstChild.index; lastChildIndex = lastChild.index; _handleScrollToIndex( completer: completer, ctx: ctx, obj: obj, index: index, alignment: alignment, firstChildIndex: firstChildIndex, lastChildIndex: lastChildIndex, padding: padding, duration: duration, curve: curve, offset: offset, onPrepareScrollToIndex: onPrepareScrollToIndex, ); } /// Scrolling to the specified index location when the child widgets have a /// fixed height. _handleScrollToIndexForFixedHeight({ required Completer completer, required BuildContext ctx, required RenderSliverMultiBoxAdaptor obj, required int index, required double alignment, required EdgeInsets padding, Duration? duration, Curve? curve, required ObserverLocateIndexOffsetCallback? offset, required ObserverRenderSliverType? renderSliverType, required ObserverOnPrepareScrollToIndex? onPrepareScrollToIndex, }) async { assert(controller != null); var _controller = controller; if (_controller == null || !_controller.hasClients) { _handleScrollInterruption(context: ctx, completer: completer); return; } bool isAnimateTo = (duration != null) && (curve != null); final targetChild = findCurrentFirstChild(obj); if (targetChild == null) { _handleScrollInterruption(context: ctx, completer: completer); return; } ObserveScrollToIndexFixedHeightResultModel resultModel; if (ListViewObserver.isSupportRenderSliverType(obj) || renderSliverType == ObserverRenderSliverType.list) { // ListView resultModel = _calculateScrollToIndexForFixedHeightResultForList( obj: obj, targetChild: targetChild, index: index, ); } else if (obj is RenderSliverGrid || renderSliverType == ObserverRenderSliverType.grid) { // GirdView resultModel = _calculateScrollToIndexForFixedHeightResultForGrid( obj: obj, targetChild: targetChild, index: index, ); } else { // Other _handleScrollInterruption(context: ctx, completer: completer); return; } _handleScrollDecision(context: ctx); double childMainAxisSize = resultModel.childMainAxisSize; double childLayoutOffset = resultModel.targetChildLayoutOffset; _updateIndexOffsetMap( ctx: ctx, index: index, childLayoutOffset: childLayoutOffset, childSize: childMainAxisSize, ); // Getting safety layout offset. final calcResult = _calculateTargetLayoutOffset( obj: obj, childLayoutOffset: childLayoutOffset, childSize: childMainAxisSize, alignment: alignment, padding: padding, offset: offset, ); childLayoutOffset = calcResult.calculateTargetLayoutOffset; await _scrollTo( isAnimateTo: isAnimateTo, duration: isAnimateTo ? duration : null, curve: isAnimateTo ? curve : null, controller: _controller, calcResult: calcResult, onPrepareScrollToIndex: onPrepareScrollToIndex, ); _handleScrollEnd(context: ctx, completer: completer); } /// Scrolling to the specified index location by gradually scrolling around /// the target index location. _handleScrollToIndex({ required Completer completer, required BuildContext ctx, required RenderSliverMultiBoxAdaptor obj, required int index, required double alignment, required int firstChildIndex, required int lastChildIndex, required EdgeInsets padding, Duration? duration, Curve? curve, ObserverLocateIndexOffsetCallback? offset, double? lastPageTurningOffset, ObserverOnPrepareScrollToIndex? onPrepareScrollToIndex, }) async { var _controller = controller; if (_controller == null || !_controller.hasClients) { _handleScrollInterruption(context: ctx, completer: completer); return; } final viewport = _findViewport(obj); if (viewport == null) { _handleScrollInterruption(context: ctx, completer: completer); return; } final extremeScrollExtent = viewportExtremeScrollExtent( viewport: viewport, obj: obj, ); final maxScrollExtent = extremeScrollExtent.rectify(obj); final isHorizontal = obj.constraints.axis == Axis.horizontal; bool isAnimateTo = (duration != null) && (curve != null); final precedingScrollExtent = obj.constraints.precedingScrollExtent; if (index < firstChildIndex) { final sliverSize = isHorizontal ? obj.paintBounds.width : obj.paintBounds.height; double childLayoutOffset = 0; final firstChild = findCurrentFirstChild(obj); final parentData = firstChild?.parentData; if (parentData is SliverMultiBoxAdaptorParentData) { childLayoutOffset = parentData.layoutOffset ?? 0; } var targetLeadingOffset = childLayoutOffset - sliverSize; if (targetLeadingOffset < 0) { targetLeadingOffset = 0; } double prevPageOffset = targetLeadingOffset + precedingScrollExtent; prevPageOffset = prevPageOffset < 0 ? 0 : prevPageOffset; // The offset of this page turning is the same as the previous one, // which means the [index] is wrong. if (lastPageTurningOffset == prevPageOffset) { Log.warning('The child corresponding to the index cannot be found.\n' 'Please make sure the index is correct.'); _handleScrollInterruption(context: ctx, completer: completer); return; } lastPageTurningOffset = prevPageOffset; final prevPageOffsetRectified = prevPageOffset.rectify(obj); if (isAnimateTo) { await _controller.animateTo( prevPageOffsetRectified, duration: _findingDuration, curve: _findingCurve, ); } else { _controller.jumpTo(prevPageOffsetRectified); } ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) { final firstChild = findCurrentFirstChild(obj); final lastChild = findCurrentLastChild(obj); if (firstChild == null || lastChild == null) { _handleScrollInterruption(context: ctx, completer: completer); return; } firstChildIndex = firstChild.index; lastChildIndex = lastChild.index; _handleScrollToIndex( completer: completer, ctx: ctx, obj: obj, index: index, alignment: alignment, firstChildIndex: firstChildIndex, lastChildIndex: lastChildIndex, padding: padding, duration: duration, curve: curve, offset: offset, lastPageTurningOffset: lastPageTurningOffset, onPrepareScrollToIndex: onPrepareScrollToIndex, ); }); } else if (index > lastChildIndex) { final lastChild = findCurrentLastChild(obj); final childSize = (isHorizontal ? lastChild?.paintBounds.width : lastChild?.paintBounds.height) ?? 0; double childLayoutOffset = 0; final parentData = lastChild?.parentData; if (parentData is SliverMultiBoxAdaptorParentData) { childLayoutOffset = parentData.layoutOffset ?? 0; } double nextPageOffset = childLayoutOffset + childSize + precedingScrollExtent; nextPageOffset = nextPageOffset > maxScrollExtent ? maxScrollExtent : nextPageOffset; // The offset of this page turning is the same as the previous one, // which means the [index] is wrong. if (lastPageTurningOffset == nextPageOffset) { Log.warning('The child corresponding to the index cannot be found.\n' 'Please make sure the index is correct.'); _handleScrollInterruption(context: ctx, completer: completer); return; } lastPageTurningOffset = nextPageOffset; final nextPageOffsetRectified = nextPageOffset.rectify(obj); if (isAnimateTo) { await _controller.animateTo( nextPageOffsetRectified, duration: _findingDuration, curve: _findingCurve, ); } else { _controller.jumpTo(nextPageOffsetRectified); } ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) { final firstChild = findCurrentFirstChild(obj); final lastChild = findCurrentLastChild(obj); if (firstChild == null || lastChild == null) { _handleScrollInterruption(context: ctx, completer: completer); return; } firstChildIndex = firstChild.index; lastChildIndex = lastChild.index; _handleScrollToIndex( completer: completer, ctx: ctx, obj: obj, index: index, alignment: alignment, firstChildIndex: firstChildIndex, lastChildIndex: lastChildIndex, padding: padding, duration: duration, curve: curve, offset: offset, lastPageTurningOffset: lastPageTurningOffset, onPrepareScrollToIndex: onPrepareScrollToIndex, ); }); } else { // Target index child is already in viewport var targetChild = obj.firstChild; while (targetChild != null) { if (targetChild is! RenderIndexedSemantics) { targetChild = obj.childAfter(targetChild); continue; } final currentChildIndex = targetChild.index; double childLayoutOffset = 0; final parentData = targetChild.parentData; if (parentData is SliverMultiBoxAdaptorParentData) { childLayoutOffset = parentData.layoutOffset ?? 0; } final isHorizontal = obj.constraints.axis == Axis.horizontal; final childPaintBounds = targetChild.paintBounds; final childSize = isHorizontal ? childPaintBounds.width : childPaintBounds.height; _updateIndexOffsetMap( ctx: ctx, index: currentChildIndex, childLayoutOffset: childLayoutOffset, childSize: childSize, ); if (currentChildIndex != index) { targetChild = obj.childAfter(targetChild); continue; } else { _handleScrollDecision(context: ctx); final calcResult = _calculateTargetLayoutOffset( obj: obj, childLayoutOffset: childLayoutOffset, childSize: childSize, alignment: alignment, padding: padding, offset: offset, ); await _scrollTo( isAnimateTo: isAnimateTo, duration: isAnimateTo ? duration : null, curve: isAnimateTo ? curve : null, controller: _controller, calcResult: calcResult, onPrepareScrollToIndex: onPrepareScrollToIndex, ); _handleScrollEnd(context: ctx, completer: completer); } break; } } } Future _scrollTo({ required bool isAnimateTo, required Duration? duration, required Curve? curve, required ScrollController controller, required ObservePrepareScrollToIndexModel calcResult, required ObserverOnPrepareScrollToIndex? onPrepareScrollToIndex, }) async { assert(controller.hasClients); // The developer has handled it externally. final haveHandle = await onPrepareScrollToIndex?.call(calcResult) ?? false; if (haveHandle) return; final targetOffset = calcResult.calculateTargetLayoutOffset; if (isAnimateTo) { assert(duration != null); assert(curve != null); await controller.animateTo( targetOffset, duration: duration ?? const Duration(milliseconds: 1), curve: curve ?? Curves.linear, ); } else { controller.jumpTo(targetOffset); } } /// Getting target safety layout offset for scrolling to index. /// This can avoid jitter. ObservePrepareScrollToIndexModel _calculateTargetLayoutOffset({ required RenderSliverMultiBoxAdaptor obj, required double childLayoutOffset, required double childSize, required double alignment, required EdgeInsets padding, ObserverLocateIndexOffsetCallback? offset, }) { final precedingScrollExtent = obj.constraints.precedingScrollExtent; double targetItemLeadingPadding = childSize * alignment; var targetOffset = childLayoutOffset + precedingScrollExtent + targetItemLeadingPadding; double scrollOffset = 0; double remainingBottomExtent = 0; double needScrollExtent = 0; if (this is SliverObserverController) { final viewport = _findViewport(obj); if (viewport != null && viewport.offset.hasPixels) { scrollOffset = viewport.offset.pixels.rectify(obj); final extremeScrollExtent = viewportExtremeScrollExtent( viewport: viewport, obj: obj, ); final maxScrollExtent = extremeScrollExtent.rectify(obj); remainingBottomExtent = maxScrollExtent - scrollOffset; needScrollExtent = childLayoutOffset + precedingScrollExtent + targetItemLeadingPadding - scrollOffset; } } else { final constraints = obj.constraints; final isVertical = constraints.axis == Axis.vertical; final trailingPadding = isVertical ? padding.bottom : padding.right; final viewportExtent = constraints.viewportMainAxisExtent; final geometry = obj.geometry; // The (estimated) total scrollable extent of this sliver. double scrollExtent = geometry?.scrollExtent ?? 0; scrollOffset = obj.constraints.scrollOffset; remainingBottomExtent = scrollExtent + precedingScrollExtent + trailingPadding - scrollOffset - viewportExtent; needScrollExtent = childLayoutOffset + precedingScrollExtent + targetItemLeadingPadding - scrollOffset; } final outerOffset = offset?.call(targetOffset) ?? 0; needScrollExtent = needScrollExtent - outerOffset; // The bottom remaining distance is satisfied to go completely scrolling. bool isEnoughScroll = remainingBottomExtent >= needScrollExtent; if (!isEnoughScroll) { targetOffset = remainingBottomExtent + scrollOffset; } else { targetOffset = needScrollExtent + scrollOffset; } // The remainingBottomExtent may be negative when the scrollView has too // few items. targetOffset = targetOffset.clamp(0, double.maxFinite); final calculateTargetLayoutOffset = targetOffset.rectify(obj); return ObservePrepareScrollToIndexModel( calculateTargetLayoutOffset: calculateTargetLayoutOffset, precedingScrollExtent: precedingScrollExtent, targetChildLayoutOffset: childLayoutOffset, ); } /// Calculate the information about scrolling to the specified index location /// when the type is ObserverRenderSliverType.list. ObserveScrollToIndexFixedHeightResultModel _calculateScrollToIndexForFixedHeightResultForList({ required RenderSliverMultiBoxAdaptor obj, required RenderIndexedSemantics targetChild, required int index, }) { final childPaintBounds = targetChild.paintBounds; final isHorizontal = obj.constraints.axis == Axis.horizontal; // The separator size between items on the main axis. double itemSeparatorHeight = 0; /// The size of item on the main axis. final childMainAxisSize = isHorizontal ? childPaintBounds.width : childPaintBounds.height; var nextChild = obj.childAfter(targetChild); nextChild ??= obj.childBefore(targetChild); if (nextChild != null && nextChild is! RenderIndexedSemantics) { // It is separator final nextChildPaintBounds = nextChild.paintBounds; itemSeparatorHeight = isHorizontal ? nextChildPaintBounds.width : nextChildPaintBounds.height; } // Calculate the offset of the target child widget on the main axis. double targetChildLayoutOffset = (childMainAxisSize + itemSeparatorHeight) * index; return ObserveScrollToIndexFixedHeightResultModel( childMainAxisSize: childMainAxisSize, itemSeparatorHeight: itemSeparatorHeight, indexOfLine: index, targetChildLayoutOffset: targetChildLayoutOffset, ); } /// Calculate the information about scrolling to the specified index location /// when the type is ObserverRenderSliverType.grid. ObserveScrollToIndexFixedHeightResultModel _calculateScrollToIndexForFixedHeightResultForGrid({ required RenderSliverMultiBoxAdaptor obj, required RenderIndexedSemantics targetChild, required int index, }) { final childPaintBounds = targetChild.paintBounds; final isHorizontal = obj.constraints.axis == Axis.horizontal; // The separator size between items on the main axis. double itemSeparatorHeight = 0; // The number of rows for the target item. int indexOfLine = index; /// The size of item on the main axis. final childMainAxisSize = isHorizontal ? childPaintBounds.width : childPaintBounds.height; double crossAxisSpacing = 0; bool isHaveSetCrossAxisSpacing = false; var nextChild = obj.childAfter(targetChild); // Find the next child that is not on the same line and calculate the // mainAxisSpacing. var nextChildOrigin = nextChild?.localToGlobal(Offset.zero) ?? Offset.zero; final targetChildOrigin = targetChild.localToGlobal(Offset.zero); while (nextChild != null && (isHorizontal ? nextChildOrigin.dx == targetChildOrigin.dx : nextChildOrigin.dy == targetChildOrigin.dy)) { if (!isHaveSetCrossAxisSpacing) { // Find the next child on the same line and calculate the // crossAxisSpacing. if (isHorizontal) { crossAxisSpacing = (nextChildOrigin.dy - targetChildOrigin.dy).abs() - childPaintBounds.height; } else { crossAxisSpacing = (nextChildOrigin.dx - targetChildOrigin.dx).abs() - childPaintBounds.width; } isHaveSetCrossAxisSpacing = true; } nextChild = obj.childAfter(nextChild); nextChildOrigin = nextChild?.localToGlobal(Offset.zero) ?? Offset.zero; } if (nextChild != null) { if (isHorizontal) { itemSeparatorHeight = (nextChildOrigin.dx - targetChildOrigin.dx).abs() - childPaintBounds.width; } else { itemSeparatorHeight = (nextChildOrigin.dy - targetChildOrigin.dy).abs() - childPaintBounds.height; } } else { // Can't find the next child that is not on the same line. // Find the before child that is not on the same line and calculate the // mainAxisSpacing. var previousChild = obj.childBefore(targetChild); var previousChildOrigin = previousChild?.localToGlobal(Offset.zero) ?? Offset.zero; while (previousChild != null && (isHorizontal ? previousChildOrigin.dx == targetChildOrigin.dx : previousChildOrigin.dy == targetChildOrigin.dy)) { if (!isHaveSetCrossAxisSpacing) { // Find two child on the same line and calculate the // crossAxisSpacing. double firstBeforeCrossAxisOrigin = isHorizontal ? previousChildOrigin.dy : previousChildOrigin.dx; previousChild = obj.childBefore(previousChild); previousChildOrigin = previousChild?.localToGlobal(Offset.zero) ?? Offset.zero; if (previousChild != null) { double secondBeforeCrossAxisOrigin = isHorizontal ? previousChildOrigin.dy : previousChildOrigin.dx; crossAxisSpacing = (firstBeforeCrossAxisOrigin - secondBeforeCrossAxisOrigin) .abs() - (isHorizontal ? childPaintBounds.height : childPaintBounds.width); isHaveSetCrossAxisSpacing = true; } } else { previousChild = obj.childBefore(previousChild); previousChildOrigin = previousChild?.localToGlobal(Offset.zero) ?? Offset.zero; } } if (previousChild != null) { if (isHorizontal) { itemSeparatorHeight = (targetChildOrigin.dx - previousChildOrigin.dx).abs() - childPaintBounds.width; } else { itemSeparatorHeight = (targetChildOrigin.dy - previousChildOrigin.dy).abs() - childPaintBounds.height; } } } final childCrossAxisSize = isHorizontal ? childPaintBounds.height : childPaintBounds.width; // Calculate the number of lines. // round() for avoiding precision errors. int itemsPerLine = ((obj.constraints.crossAxisExtent + crossAxisSpacing) / (childCrossAxisSize + crossAxisSpacing)) .round(); // Calculate the number of lines. indexOfLine = (index / itemsPerLine).floor(); // Calculate the offset of the target child widget on the main axis. double targetChildLayoutOffset = (childMainAxisSize + itemSeparatorHeight) * indexOfLine; return ObserveScrollToIndexFixedHeightResultModel( childMainAxisSize: childMainAxisSize, itemSeparatorHeight: itemSeparatorHeight, indexOfLine: indexOfLine, targetChildLayoutOffset: targetChildLayoutOffset, ); } /// Update the [indexOffsetMap] property. _updateIndexOffsetMap({ required BuildContext ctx, required int index, required double childLayoutOffset, required double childSize, }) { // No need to cache if (!cacheJumpIndexOffset) return; // To cache offset final map = indexOffsetMap[ctx] ?? {}; map[index] = ObserveScrollChildModel( layoutOffset: childLayoutOffset, size: childSize, ); indexOffsetMap[ctx] = map; } /// Called when starting the scrolling task. _handleScrollStart({ required BuildContext? context, }) { innerIsHandlingScroll = true; ObserverScrollStartNotification().dispatch(context); } /// Called when the scrolling task is interrupted. /// /// For example, the conditions are not met, or the item with the specified /// index cannot be found, etc. _handleScrollInterruption({ required BuildContext? context, required Completer completer, }) { innerIsHandlingScroll = false; completer.complete(); ObserverScrollInterruptionNotification().dispatch(context); } /// Called when the item with the specified index has been found. _handleScrollDecision({ required BuildContext? context, }) { ObserverScrollDecisionNotification().dispatch(context); } /// Called after completing the scrolling task. _handleScrollEnd({ required BuildContext? context, required Completer completer, }) { if (innerNeedOnceObserveCallBack != null) { ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) { innerIsHandlingScroll = false; innerNeedOnceObserveCallBack!(); completer.complete(); ObserverScrollEndNotification().dispatch(context); }); } else { innerIsHandlingScroll = false; completer.complete(); ObserverScrollEndNotification().dispatch(context); } } } ================================================ FILE: lib/src/common/observer_listener.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-10-27 17:16:57 */ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:scrollview_observer/src/common/models/observe_model.dart'; import 'package:scrollview_observer/src/common/observer_typedef.dart'; class ObserverListenerEntry extends LinkedListEntry> { ObserverListenerEntry({ required this.context, required this.onObserve, required this.onObserveAll, }); /// The context of the listener. final BuildContext? context; /// The callback of getting observed result. final OnObserveCallback? onObserve; /// The callback of getting observed result map. final OnObserveAllCallback? onObserveAll; } ================================================ FILE: lib/src/common/observer_notification_result.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-08-12 20:09:46 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/src/common/models/observe_model.dart'; import 'package:scrollview_observer/src/common/models/observer_handle_contexts_result_model.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; class CommonOnceObserveNotificationResult> { bool get isSuccess => ObserverWidgetObserveResultType.success == type; /// Observation result type. final ObserverWidgetObserveResultType type; /// Observation result for first sliver. /// Corresponding to [onObserve] in [ObserverWidget]. final M? observeResult; /// Observation result map. /// Corresponding to [onObserveAll] in [ObserverWidget]. final Map observeAllResult; CommonOnceObserveNotificationResult({ required this.type, required this.observeResult, required this.observeAllResult, }); } ================================================ FILE: lib/src/common/observer_typedef.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-12-04 15:57:38 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/src/common/models/observe_model.dart'; import 'package:scrollview_observer/src/common/models/observe_scroll_to_index_result_model.dart'; import 'package:scrollview_observer/src/sliver/models/sliver_viewport_observe_model.dart'; /// Called when the ObserverController prepare to scroll to index with /// [ObservePrepareScrollToIndexModel]. typedef ObserverOnPrepareScrollToIndex = Future Function( ObservePrepareScrollToIndexModel); /// The callback type of getting observed result for first sliver. /// /// Corresponds to onObserve. typedef OnObserveCallback = void Function( M result, ); /// The callback type of getting observed result map. /// /// Corresponds to onObserveAll. typedef OnObserveAllCallback = void Function( Map resultMap, ); /// The callback type of getting all slivers those are displayed in viewport. /// /// Corresponds to onObserveViewport. typedef OnObserveViewportCallback = void Function( SliverViewportObserveModel result, ); /// Define type that auto trigger observe. enum ObserverAutoTriggerObserveType { scrollStart, scrollUpdate, scrollEnd, } /// Define type that trigger [onObserve] callback. enum ObserverTriggerOnObserveType { directly, displayingItemsChange, } /// Define type of the observed render sliver. enum ObserverRenderSliverType { /// listView list, /// gridView grid, } ================================================ FILE: lib/src/common/observer_widget.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'dart:async'; import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/src/common/models/observer_handle_contexts_result_model.dart'; import 'package:scrollview_observer/src/common/observer_controller.dart'; import 'package:scrollview_observer/src/common/observer_listener.dart'; import 'package:scrollview_observer/src/common/observer_typedef.dart'; import 'package:scrollview_observer/src/common/observer_widget_scope.dart'; import 'package:scrollview_observer/src/common/observer_widget_tag_manager.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; import 'package:scrollview_observer/src/notification.dart'; import 'package:scrollview_observer/src/utils/src/log.dart'; import 'models/observe_model.dart'; class ObserverWidget extends StatefulWidget { /// The subtree below this widget. final Widget child; /// This is for when you have multiple nested [ObserverWidget] widgets and /// you want to get the corresponding [ObserverWidgetState]. /// /// It must be a unique string in the tree. final String? tag; /// An object that can be used to dispatch a [ListViewOnceObserveNotification] /// or [GridViewOnceObserveNotification]. final C? sliverController; /// The callback of getting all sliver's buildContext. final List Function()? sliverContexts; /// The callback of getting observed result map. final OnObserveAllCallback? onObserveAll; /// The callback of getting observed result for first sliver. final OnObserveCallback? onObserve; /// Calculate offset. final double leadingOffset; /// Calculate offset dynamically /// If this callback is implemented, the [leadingOffset] property will be /// invalid. final double Function()? dynamicLeadingOffset; /// After the internal logic figure out the first child widget, if the /// proportion of the size of the child widget blocked to its own size exceeds /// the value [toNextOverPercent], the next child widget will be the first /// child widget. final double toNextOverPercent; /// A predicate for [ScrollNotification], used to determine whether /// observation can be triggered. /// /// Generally combined with [defaultScrollNotificationPredicate] to check /// whether `notification.depth == 0`, which means that the notification did /// not bubble through any intervening scrolling widgets. /// This can avoid the unnecessary observation calculations caused by /// intervening scrolling widgets, which in turn improves performance. final ScrollNotificationPredicate? scrollNotificationPredicate; /// Used to set types those can trigger observe automatically. /// /// Defaults to [.scrollStart, .scrollUpdate, .scrollEnd] final List? autoTriggerObserveTypes; /// Used to set the prerequisite for triggering the [onObserve] callback. /// /// Defaults to [ObserverTriggerOnObserveType.displayingItemsChange]. final ObserverTriggerOnObserveType triggerOnObserveType; /// Used to find the target RenderSliver. /// /// The default is to find [RenderSliverList], [RenderSliverFixedExtentList] /// and [RenderSliverGrid]. final bool Function(RenderObject?)? customTargetRenderSliverType; /// It allows you to customize observation logic when original logic doesn't /// fit your needs. final M? Function(BuildContext)? customHandleObserve; const ObserverWidget({ Key? key, required this.child, this.tag, this.sliverController, this.sliverContexts, this.onObserveAll, this.onObserve, this.leadingOffset = 0, this.dynamicLeadingOffset, this.toNextOverPercent = 1, this.scrollNotificationPredicate, this.autoTriggerObserveTypes, this.triggerOnObserveType = ObserverTriggerOnObserveType.displayingItemsChange, this.customHandleObserve, this.customTargetRenderSliverType, }) : assert(toNextOverPercent > 0 && toNextOverPercent <= 1), super(key: key); @override State createState() => ObserverWidgetState>(); /// Returning the closest instance of this class that encloses the given /// context. /// /// If you give a tag, it will give priority find the corresponding instance /// of this class with the given tag and return it. /// /// If there is no [ObserverWidget] widget, then null is returned. /// /// Calling this method will create a dependency on the closest /// [ObserverWidget] in the [context], if there is one. /// /// See also: /// /// * [ObserverWidget.of], which is similar to this method, but asserts if no /// [ObserverWidget] instance is found. static ObserverWidgetState? maybeOf< C extends ObserverController, M extends ObserveModel, N extends ScrollViewOnceObserveNotification, T extends ObserverWidget>( BuildContext context, { String? tag, }) { BuildContext? _ctx; if (tag != null) { final tagManager = ObserverWidgetTagManager.maybeOf(context); _ctx = tagManager?.context(tag); } return (_ctx ?? context) .dependOnInheritedWidgetOfExactType>() ?.observerWidgetState; } /// Returning the closest instance of this class that encloses the given /// context. /// /// If you give a tag, it will give priority find the corresponding instance /// of this class with the given tag and return it. /// /// If no instance is found, this method will assert in debug mode, and throw /// an exception in release mode. /// /// Calling this method will create a dependency on the closest /// [ObserverWidget] in the [context]. /// /// See also: /// /// * [ObserverWidget.maybeOf], which is similar to this method, but returns /// null if no [ObserverWidget] instance is found. static ObserverWidgetState of< C extends ObserverController, M extends ObserveModel, N extends ScrollViewOnceObserveNotification, T extends ObserverWidget>( BuildContext context, { String? tag, }) { final observerState = maybeOf( context, tag: tag, ); assert(() { if (observerState == null) { throw FlutterError( '$T.of() was called with a context that does not contain a ' '$T widget.\n' 'No $T widget ancestor could be found starting from the ' 'context that was passed to $T.of(). This can happen ' 'because you are using a widget that looks for a $T ' 'ancestor, but no such ancestor exists.\n' 'The context used was:\n' ' $context', ); } return true; }()); return observerState!; } } class ObserverWidgetState< C extends ObserverController, M extends ObserveModel, N extends ScrollViewOnceObserveNotification, T extends ObserverWidget> extends State { /// Target sliver [BuildContext] List targetSliverContexts = []; /// The last observation result Map lastResultMap = {}; /// Default values for the widget's autoTriggerObserveTypes property. List get innerAutoTriggerObserveTypes => widget.autoTriggerObserveTypes ?? [ ObserverAutoTriggerObserveType.scrollStart, ObserverAutoTriggerObserveType.scrollUpdate, ObserverAutoTriggerObserveType.scrollEnd ]; /// Mapping [ObserverAutoTriggerObserveType] to [ScrollNotification]. List get innerAutoTriggerObserveScrollNotifications => innerAutoTriggerObserveTypes.map((type) { switch (type) { case ObserverAutoTriggerObserveType.scrollStart: return ScrollStartNotification; case ObserverAutoTriggerObserveType.scrollUpdate: return ScrollUpdateNotification; case ObserverAutoTriggerObserveType.scrollEnd: return ScrollEndNotification; } }).toList(); /// Whether can handle observe. bool innerCanHandleObserve = true; /// The [BuildContext] of the [ObserverWidgetScope]. BuildContext? scopeContext; /// The listener list state for a [ObserverWidget] returned by /// [ObserverWidget.of]. /// /// It supports a listener list instead of just a single observation /// callback (such as onObserve and onObserveAll). @protected @visibleForTesting LinkedList>? innerListeners = LinkedList>(); /// The number of times the tag has changed. @protected @visibleForTesting int innerTagChangeCount = 0; /// The future that completes at the end of the current frame, used in /// [_checkTagChange]. /// This can be assigned in tests for custom behavior. @protected @visibleForTesting Future? innerCheckTagChangeEndOfFrame; bool _debugAssertNotDisposed() { assert(() { if (innerListeners == null) { throw FlutterError( 'A $runtimeType was used after being disposed.\n' 'Once you have called dispose() on a $runtimeType, it can no longer be used.', ); } return true; }()); return true; } @override void initState() { super.initState(); _setupSliverController(isInitState: true); } @override void didUpdateWidget(covariant T oldWidget) { super.didUpdateWidget(oldWidget); _checkTagChange(oldWidget); } @override void dispose() { assert(_debugAssertNotDisposed()); innerListeners?.clear(); innerListeners = null; super.dispose(); } @override Widget build(BuildContext context) { // Placed at the deepest level for convenient subsequent operations using // its context. Widget resultWidget = ObserverWidgetScope( child: widget.child, observerWidgetState: this, onCreateElement: _handleScopeContext, ); resultWidget = NotificationListener( onNotification: (notification) { final result = handleContexts( isForceObserve: notification.isForce, isFromObserveNotification: true, isDependObserveCallback: notification.isDependObserveCallback, ); final sliverController = widget.sliverController; if (sliverController is ObserverControllerForNotification) { sliverController.innerHandleDispatchOnceObserveComplete( resultModel: result, ); } return true; }, child: NotificationListener( onNotification: (notification) { // If the scrollNotificationPredicate returns false, the notification // will be ignored. if (!(widget.scrollNotificationPredicate?.call(notification) ?? true)) { return false; } // If the notification.runtimeType is not in the list of // innerAutoTriggerObserveScrollNotifications that can trigger // observation, the notification will be ignored. if (innerAutoTriggerObserveScrollNotifications .contains(notification.runtimeType)) { final isIgnoreInnerCanHandleObserve = ScrollUpdateNotification != notification.runtimeType; WidgetsBinding.instance.endOfFrame.then((_) { // Need to wait for frame end to avoid inaccurate observation // result, reasons as follows // // ======================== WEB ======================== // // Getting bad observation result because scrolling in Flutter Web // with mouse wheel is not smooth. // https://github.com/flutter/flutter/issues/78708 // https://github.com/flutter/flutter/issues/78634 // // issue // https://github.com/LinXunFeng/flutter_scrollview_observer/issues/31 // // ======================== APP ======================== // // When using ScrollController's animateTo with a value exceeding // the maximum scroll range, it will lead to inaccurate // observation result. // // issue // https://github.com/fluttercandies/flutter_scrollview_observer/issues/113 handleContexts( isIgnoreInnerCanHandleObserve: isIgnoreInnerCanHandleObserve, ); }); } return false; }, child: resultWidget, ), ); // When nesting multiple ObserverWidgets, ensure that only one // ObserverWidgetTagManager is at the top. if (ObserverWidgetTagManager.maybeOf(context) == null) { resultWidget = ObserverWidgetTagManager( child: resultWidget, ); } return resultWidget; } /// Setup sliver controller _setupSliverController({bool isInitState = false}) { final sliverController = widget.sliverController; if (sliverController == null) return; sliverController.innerReset(); sliverController.innerNeedOnceObserveCallBack = () { handleContexts(); }; sliverController.innerReattachCallBack = () { targetSliverContexts.clear(); _setupSliverController(); }; ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { targetSliverContexts = fetchTargetSliverContexts(); sliverController.sliverContexts = targetSliverContexts; if (isInitState && sliverController is ObserverControllerForScroll) { sliverController.innerInitialIndexPosition(); } }); } /// Fetch target sliver [BuildContext]s List fetchTargetSliverContexts() { List ctxs = targetSliverContexts; if (ctxs.isEmpty) { final sliverListContexts = widget.sliverContexts; if (sliverListContexts != null) { ctxs = sliverListContexts(); } else { List _ctxs = []; void visitor(Element element) { if (isTargetSliverContextType(element.renderObject)) { /// Find the target sliver context _ctxs.add(element); return; } element.visitChildren(visitor); } try { // https://github.com/LinXunFeng/flutter_scrollview_observer/issues/35 context.visitChildElements(visitor); } catch (e) { Log.warning( 'This widget has been unmounted, so the State no longer has a context (and should be considered defunct). \n' 'Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.', ); } ctxs = _ctxs; } } return ctxs; } /// Fetch offset from [leadingOffset] or [dynamicLeadingOffset]. double fetchLeadingOffset() { var offset = widget.leadingOffset; if (widget.dynamicLeadingOffset != null) { offset = widget.dynamicLeadingOffset!(); } return offset; } /// Determine whether it is the type of the target sliver. bool isTargetSliverContextType(RenderObject? obj) { if (widget.customTargetRenderSliverType != null) { return widget.customTargetRenderSliverType!.call(obj); } return obj is RenderSliverList; } /// Update [innerCanHandleObserve] according to the /// [ObserverController.observeIntervalForScrolling]. updateInnerCanHandleObserve() async { final observeInterval = widget.sliverController?.observeIntervalForScrolling ?? Duration.zero; if (Duration.zero == observeInterval) { innerCanHandleObserve = true; return; } if (!innerCanHandleObserve) return; innerCanHandleObserve = false; await Future.delayed(observeInterval); innerCanHandleObserve = true; } /// Handle all buildContext ObserverHandleContextsResultModel? handleContexts({ bool isForceObserve = false, bool isFromObserveNotification = false, bool isDependObserveCallback = true, bool isIgnoreInnerCanHandleObserve = true, }) { if (!isIgnoreInnerCanHandleObserve) { if (!innerCanHandleObserve) { return null; } updateInnerCanHandleObserve(); } final isForbidObserveCallback = widget.sliverController?.isForbidObserveCallback ?? false; final onObserve = isForbidObserveCallback ? null : widget.onObserve; final onObserveAll = isForbidObserveCallback ? null : widget.onObserveAll; if (isDependObserveCallback) { if (onObserve == null && onObserveAll == null && (innerListeners?.isEmpty ?? true)) { return null; } } final isHandlingScroll = widget.sliverController?.innerIsHandlingScroll ?? false; if (isHandlingScroll) { return null; } List ctxs = fetchTargetSliverContexts(); Map resultMap = {}; Map changeResultMap = {}; M? changeResultModel; for (var i = 0; i < ctxs.length; i++) { final ctx = ctxs[i]; final targetObserveModel = handleObserve(ctx); if (targetObserveModel == null) continue; resultMap[ctx] = targetObserveModel; if (isForceObserve || widget.triggerOnObserveType == ObserverTriggerOnObserveType.directly) { changeResultMap[ctx] = targetObserveModel; } else { final lastResultModel = lastResultMap[ctx]; if (lastResultModel == null) { changeResultMap[ctx] = targetObserveModel; } else if (lastResultModel != targetObserveModel) { changeResultMap[ctx] = targetObserveModel; } } // Getting observed result for first listView. if (i == 0 && changeResultMap[ctx] != null) { changeResultModel = changeResultMap[ctx]; } } lastResultMap = resultMap; if (isDependObserveCallback && onObserve != null && changeResultModel != null) { onObserve(changeResultModel); } if (isDependObserveCallback && onObserveAll != null && changeResultMap.isNotEmpty) { onObserveAll(changeResultMap); } _notifyListeners(changeResultMap); return ObserverHandleContextsResultModel( changeResultModel: changeResultModel, changeResultMap: changeResultMap, ); } M? handleObserve(BuildContext ctx) { if (widget.customHandleObserve != null) { return widget.customHandleObserve?.call(ctx); } return null; } void _handleScopeContext(BuildContext ctx) async { scopeContext = ctx; final tag = widget.tag ?? ''; if (tag.isEmpty) return; await WidgetsBinding.instance.endOfFrame; assert(ctx.mounted); final tagManager = ObserverWidgetTagManager.maybeOf(ctx); tagManager?.set(tag, ctx); } void _checkTagChange(T oldWidget) async { final oldTag = oldWidget.tag ?? ''; final tag = widget.tag ?? ''; if (tag == oldTag) return; innerTagChangeCount++; // Execute after the current frame ends to avoid getting an outdated // ObserverWidgetTagManager. await (innerCheckTagChangeEndOfFrame ?? WidgetsBinding.instance.endOfFrame); if (!mounted) return; final _scopeContext = scopeContext; if (_scopeContext == null) return; assert(_scopeContext.mounted); final tagManager = ObserverWidgetTagManager.maybeOf(_scopeContext); tagManager?.remove(oldTag); if (tag.isNotEmpty) { tagManager?.set(tag, _scopeContext); } } /// Add [OnObserveCallback] and [OnObserveAllCallback] that will be called /// each time a result is observed. void addListener({ BuildContext? context, OnObserveCallback? onObserve, OnObserveAllCallback? onObserveAll, }) { assert(_debugAssertNotDisposed()); assert( onObserve != null || onObserveAll != null, 'At least one callback must be provided.', ); innerListeners?.add(ObserverListenerEntry( context: context, onObserve: onObserve, onObserveAll: onObserveAll, )); } /// Remove the specified [OnObserveCallback] and [OnObserveAllCallback]. void removeListener({ BuildContext? context, OnObserveCallback? onObserve, OnObserveAllCallback? onObserveAll, }) { assert(_debugAssertNotDisposed()); assert( onObserve != null || onObserveAll != null, 'At least one callback must be provided.', ); final _listeners = innerListeners; if (_listeners == null) return; for (final ObserverListenerEntry entry in _listeners) { if (entry.context == context && entry.onObserve == onObserve && entry.onObserveAll == onObserveAll) { entry.unlink(); return; } } } void _notifyListeners( Map changeResultMap, ) { if (changeResultMap.isEmpty) return; final _listeners = innerListeners; if (_listeners == null || _listeners.isEmpty) return; final List> localListeners = List>.of(_listeners); for (final ObserverListenerEntry entry in localListeners) { try { if (entry.list != null) { entry.onObserveAll?.call(changeResultMap); if (entry.onObserve != null) { // If sliverContext is not specified, the first one in // targetSliverContexts is taken. BuildContext? _sliverContext = entry.context; if (_sliverContext == null && targetSliverContexts.isNotEmpty) { _sliverContext = targetSliverContexts.first; } final result = changeResultMap[_sliverContext]; if (result == null) continue; entry.onObserve?.call(result); } } } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'scrollview_observer', context: ErrorDescription('while dispatching result for $runtimeType'), informationCollector: () => [ DiagnosticsProperty( 'The $runtimeType sending result was', this, style: DiagnosticsTreeStyle.errorProperty, ), ], )); } } } } ================================================ FILE: lib/src/common/observer_widget_scope.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-10-19 11:49:39 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/observer_controller.dart'; import 'package:scrollview_observer/src/common/observer_widget.dart'; class ObserverWidgetScope< C extends ObserverController, M extends ObserveModel, N extends ScrollViewOnceObserveNotification, T extends ObserverWidget> extends InheritedWidget { const ObserverWidgetScope({ Key? key, required Widget child, required this.observerWidgetState, required this.onCreateElement, }) : super(key: key, child: child); /// The [ObserverWidgetState] instance. final ObserverWidgetState observerWidgetState; /// The callback of [createElement]. final Function(BuildContext) onCreateElement; @override InheritedElement createElement() { final element = super.createElement(); onCreateElement.call(element); return element; } @override bool updateShouldNotify(covariant ObserverWidgetScope oldWidget) { return observerWidgetState != oldWidget.observerWidgetState; } } ================================================ FILE: lib/src/common/observer_widget_tag_manager.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-11-03 14:40:40 */ import 'package:flutter/material.dart'; class ObserverWidgetTagManager extends InheritedWidget { final Map _tagMap = {}; ObserverWidgetTagManager({ Key? key, required Widget child, }) : super(key: key, child: child); /// Getting the [ObserverWidgetTagManager] instance. /// /// If the [ObserverWidgetTagManager] instance is not found, return null. static ObserverWidgetTagManager? maybeOf(BuildContext context) { return context .dependOnInheritedWidgetOfExactType(); } /// Setting the tag and context. void set( String tag, BuildContext context, ) { _tagMap[tag] = context; } /// Removing the tag. void remove(String tag) { _tagMap.remove(tag); } /// Getting the context by tag. BuildContext? context( String tag, ) { return _tagMap[tag]; } /// Getting all tags and contexts. @protected @visibleForTesting Map get tagMap { return _tagMap; } @override bool updateShouldNotify(covariant ObserverWidgetTagManager oldWidget) { return _tagMap != oldWidget._tagMap; } } ================================================ FILE: lib/src/common/typedefs.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-21 12:50:50 */ /// This allows a value of type T or T? /// to be treated as a value of type T?. /// /// We use this so that APIs that have become /// non-nullable can still be used with `!` and `?` /// to support older versions of the API as well. T? ambiguate(T? value) => value; /// Signature for the callback when scrolling to the specified index location /// with offset. /// For example, return the height of the sticky widget. /// /// The [targetOffset] property is the offset of the planned locate. typedef ObserverLocateIndexOffsetCallback = double Function( double targetOffset); /// Observation result types in ObserverWidget. enum ObserverWidgetObserveResultType { success, interrupted, } ================================================ FILE: lib/src/gridview/grid_observer_controller.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-07-20 00:32:40 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/models/observer_handle_contexts_result_model.dart'; import 'package:scrollview_observer/src/common/observer_controller.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; class GridObserverController extends ObserverController with ObserverControllerForInfo, ObserverControllerForScroll, ObserverControllerForNotification< GridViewObserveModel, ObserverHandleContextsResultModel, GridViewOnceObserveNotificationResult> { GridObserverController({ ScrollController? controller, }) : super(controller: controller); /// Dispatch a [GridViewOnceObserveNotification] Future dispatchOnceObserve({ BuildContext? sliverContext, bool isForce = false, bool isDependObserveCallback = true, }) { return innerDispatchOnceObserve( sliverContext: sliverContext, notification: GridViewOnceObserveNotification( isForce: isForce, isDependObserveCallback: isDependObserveCallback, ), ); } /// Observe the child which is specified index in sliver. GridViewObserveDisplayingChildModel? observeItem({ required int index, BuildContext? sliverContext, }) { final model = findChildInfo(index: index, sliverContext: sliverContext); if (model == null) return null; return GridViewObserveDisplayingChildModel( sliverGrid: model.sliver as RenderSliverGrid, viewport: model.viewport, index: model.index, renderObject: model.renderObject, ); } /// Observe the first child in sliver. /// /// Note that the first child here is not the first child being displayed in /// sliver, and it may not be displayed. GridViewObserveDisplayingChildModel? observeFirstItem({ BuildContext? sliverContext, }) { final model = findCurrentFirstChildInfo(sliverContext: sliverContext); if (model == null) return null; return GridViewObserveDisplayingChildModel( sliverGrid: model.sliver as RenderSliverGrid, viewport: model.viewport, index: model.index, renderObject: model.renderObject, ); } /// Create a observation notification result. @override GridViewOnceObserveNotificationResult innerCreateOnceObserveNotificationResult({ required ObserverWidgetObserveResultType resultType, required ObserverHandleContextsResultModel? resultModel, }) { return GridViewOnceObserveNotificationResult( type: resultType, observeResult: resultModel ?? ObserverHandleContextsResultModel(), ); } /// Jump to the specified index position with animation. /// /// If the height of the child widget and the height of the separator are /// fixed, please pass the [isFixedHeight] parameter. /// /// The [alignment] specifies the desired position for the leading edge of the /// child widget. It must be a value in the range [0.0, 1.0]. Future animateTo({ required int index, required Duration duration, required Curve curve, EdgeInsets padding = EdgeInsets.zero, BuildContext? sliverContext, bool isFixedHeight = false, double alignment = 0, ObserverLocateIndexOffsetCallback? offset, }) { return innerAnimateTo( index: index, duration: duration, curve: curve, padding: padding, sliverContext: sliverContext, isFixedHeight: isFixedHeight, alignment: alignment, offset: offset, renderSliverType: ObserverRenderSliverType.grid, ); } /// Jump to the specified index position without animation. /// /// If the height of the child widget and the height of the separator are /// fixed, please pass the [isFixedHeight] parameter. /// /// If you do not pass the [isFixedHeight] parameter, the package will /// automatically gradually scroll around the target location before /// locating, which will produce an animation. /// /// The [alignment] specifies the desired position for the leading edge of the /// child widget. It must be a value in the range [0.0, 1.0]. Future jumpTo({ required int index, BuildContext? sliverContext, bool isFixedHeight = false, double alignment = 0, EdgeInsets padding = EdgeInsets.zero, ObserverLocateIndexOffsetCallback? offset, }) { return innerJumpTo( index: index, sliverContext: sliverContext, isFixedHeight: isFixedHeight, alignment: alignment, padding: padding, offset: offset, renderSliverType: ObserverRenderSliverType.grid, ); } } ================================================ FILE: lib/src/gridview/grid_observer_notification_result.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-08-12 20:08:21 */ import 'package:scrollview_observer/src/common/models/observer_handle_contexts_result_model.dart'; import 'package:scrollview_observer/src/common/observer_notification_result.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; import 'package:scrollview_observer/src/gridview/models/gridview_observe_model.dart'; class GridViewOnceObserveNotificationResult extends CommonOnceObserveNotificationResult> { GridViewOnceObserveNotificationResult({ required ObserverWidgetObserveResultType type, required ObserverHandleContextsResultModel observeResult, }) : super( type: type, observeResult: observeResult.changeResultModel, observeAllResult: observeResult.changeResultMap, ); } ================================================ FILE: lib/src/gridview/grid_observer_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/src/common/observer_widget.dart'; import 'package:scrollview_observer/src/common/observer_typedef.dart'; import 'package:scrollview_observer/src/notification.dart'; import 'package:scrollview_observer/src/observer_core.dart'; import 'grid_observer_controller.dart'; import 'models/gridview_observe_model.dart'; class GridViewObserver extends ObserverWidget { /// The callback of getting all sliverGrid's buildContext. final List Function()? sliverGridContexts; final GridObserverController? controller; const GridViewObserver({ Key? key, required Widget child, String? tag, this.sliverGridContexts, this.controller, OnObserveAllCallback? onObserveAll, OnObserveCallback? onObserve, double leadingOffset = 0, double Function()? dynamicLeadingOffset, double toNextOverPercent = 1, ScrollNotificationPredicate? scrollNotificationPredicate, List? autoTriggerObserveTypes, ObserverTriggerOnObserveType triggerOnObserveType = ObserverTriggerOnObserveType.displayingItemsChange, GridViewObserveModel? Function(BuildContext context)? customHandleObserve, bool Function(RenderObject?)? customTargetRenderSliverType, }) : super( key: key, child: child, tag: tag, sliverContexts: sliverGridContexts, sliverController: controller, onObserveAll: onObserveAll, onObserve: onObserve, leadingOffset: leadingOffset, dynamicLeadingOffset: dynamicLeadingOffset, toNextOverPercent: toNextOverPercent, scrollNotificationPredicate: scrollNotificationPredicate, autoTriggerObserveTypes: autoTriggerObserveTypes, triggerOnObserveType: triggerOnObserveType, customHandleObserve: customHandleObserve, customTargetRenderSliverType: customTargetRenderSliverType, ); @override State createState() => GridViewObserverState(); static GridViewObserverState? maybeOf( BuildContext context, { String? tag, }) { final _state = ObserverWidget.maybeOf< GridObserverController, GridViewObserveModel, GridViewOnceObserveNotification, GridViewObserver>( context, tag: tag, ); if (_state is! GridViewObserverState) return null; return _state; } static GridViewObserverState of( BuildContext context, { String? tag, }) { final _state = ObserverWidget.of< GridObserverController, GridViewObserveModel, GridViewOnceObserveNotification, GridViewObserver>( context, tag: tag, ); return _state as GridViewObserverState; } } class GridViewObserverState extends ObserverWidgetState { @override GridViewObserveModel? handleObserve(BuildContext ctx) { if (widget.customHandleObserve != null) { return widget.customHandleObserve?.call(ctx); } return ObserverCore.handleGridObserve( context: ctx, fetchLeadingOffset: fetchLeadingOffset, toNextOverPercent: widget.toNextOverPercent, ); } /// Determine whether it is the type of the target sliver. @override bool isTargetSliverContextType(RenderObject? obj) { if (widget.customTargetRenderSliverType != null) { return widget.customTargetRenderSliverType!.call(obj); } return obj is RenderSliverGrid; } } ================================================ FILE: lib/src/gridview/models/gridview_observe_displaying_child_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-07-03 15:46:45 */ import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/src/common/models/observe_displaying_child_model.dart'; import 'package:scrollview_observer/src/common/models/observe_displaying_child_model_mixin.dart'; class GridViewObserveDisplayingChildModel extends ObserveDisplayingChildModel with ObserveDisplayingChildModelMixin { GridViewObserveDisplayingChildModel({ required this.sliverGrid, required RenderViewportBase viewport, required int index, required RenderBox renderObject, }) : super( sliver: sliverGrid, viewport: viewport, index: index, renderObject: renderObject, ); /// The target sliverGrid RenderSliverMultiBoxAdaptor sliverGrid; @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is GridViewObserveDisplayingChildModel) { return index == other.index && renderObject == other.renderObject; } else { return false; } } @override int get hashCode { return index + renderObject.hashCode; } } ================================================ FILE: lib/src/gridview/models/gridview_observe_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/src/common/models/observe_model.dart'; import 'package:scrollview_observer/src/gridview/models/gridview_observe_displaying_child_model.dart'; class GridViewObserveModel extends ObserveModel { GridViewObserveModel({ required this.sliverGrid, required RenderViewportBase viewport, required this.firstGroupChildList, required this.displayingChildModelList, required this.displayingChildModelMap, required bool visible, }) : super( visible: visible, sliver: sliverGrid, viewport: viewport, innerDisplayingChildModelList: displayingChildModelList, innerDisplayingChildModelMap: displayingChildModelMap, ); /// The target sliverGrid. RenderSliverMultiBoxAdaptor sliverGrid; /// The first group child widgets those are displaying. final List firstGroupChildList; /// Stores observing model list of displaying children widgets. final List displayingChildModelList; /// Stores observing model map of displaying children widgets. final Map displayingChildModelMap; @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is GridViewObserveModel) { return listEquals(firstGroupChildList, other.firstGroupChildList) && listEquals( displayingChildModelList, other.displayingChildModelList) && mapEquals(displayingChildModelMap, other.displayingChildModelMap); } else { return false; } } @override int get hashCode { return firstGroupChildList.hashCode + displayingChildModelList.hashCode + displayingChildModelMap.hashCode; } } ================================================ FILE: lib/src/listview/list_observer_controller.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-07-20 00:32:40 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/models/observer_handle_contexts_result_model.dart'; import 'package:scrollview_observer/src/common/observer_controller.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; class ListObserverController extends ObserverController with ObserverControllerForInfo, ObserverControllerForScroll, ObserverControllerForNotification< ListViewObserveModel, ObserverHandleContextsResultModel, ListViewOnceObserveNotificationResult> { ListObserverController({ ScrollController? controller, }) : super(controller: controller); /// Dispatch a [ListViewOnceObserveNotification] Future dispatchOnceObserve({ BuildContext? sliverContext, bool isForce = false, bool isDependObserveCallback = true, }) { return innerDispatchOnceObserve( sliverContext: sliverContext, notification: ListViewOnceObserveNotification( isForce: isForce, isDependObserveCallback: isDependObserveCallback, ), ); } /// Observe the child which is specified index in sliver. ListViewObserveDisplayingChildModel? observeItem({ required int index, BuildContext? sliverContext, }) { final model = findChildInfo(index: index, sliverContext: sliverContext); if (model == null) return null; return ListViewObserveDisplayingChildModel( sliverList: model.sliver as RenderSliverMultiBoxAdaptor, viewport: model.viewport, index: model.index, renderObject: model.renderObject, ); } /// Observe the first child in sliver. /// /// Note that the first child here is not the first child being displayed in /// sliver, and it may not be displayed. ListViewObserveDisplayingChildModel? observeFirstItem({ BuildContext? sliverContext, }) { final model = findCurrentFirstChildInfo(sliverContext: sliverContext); if (model == null) return null; return ListViewObserveDisplayingChildModel( sliverList: model.sliver as RenderSliverMultiBoxAdaptor, viewport: model.viewport, index: model.index, renderObject: model.renderObject, ); } /// Create a observation notification result. @override ListViewOnceObserveNotificationResult innerCreateOnceObserveNotificationResult({ required ObserverWidgetObserveResultType resultType, required ObserverHandleContextsResultModel? resultModel, }) { return ListViewOnceObserveNotificationResult( type: resultType, observeResult: resultModel ?? ObserverHandleContextsResultModel(), ); } /// Jump to the specified index position with animation. /// /// If the height of the child widget and the height of the separator are /// fixed, please pass the [isFixedHeight] parameter. /// /// The [alignment] specifies the desired position for the leading edge of the /// child widget. It must be a value in the range [0.0, 1.0]. Future animateTo({ required int index, required Duration duration, required Curve curve, EdgeInsets padding = EdgeInsets.zero, BuildContext? sliverContext, bool isFixedHeight = false, double alignment = 0, ObserverLocateIndexOffsetCallback? offset, }) { return innerAnimateTo( index: index, duration: duration, curve: curve, padding: padding, sliverContext: sliverContext, isFixedHeight: isFixedHeight, alignment: alignment, offset: offset, renderSliverType: ObserverRenderSliverType.list, ); } /// Jump to the specified index position without animation. /// /// If the height of the child widget and the height of the separator are /// fixed, please pass the [isFixedHeight] parameter. /// /// If you do not pass the [isFixedHeight] parameter, the package will /// automatically gradually scroll around the target location before /// locating, which will produce an animation. /// /// The [alignment] specifies the desired position for the leading edge of the /// child widget. It must be a value in the range [0.0, 1.0]. Future jumpTo({ required int index, BuildContext? sliverContext, bool isFixedHeight = false, double alignment = 0, EdgeInsets padding = EdgeInsets.zero, ObserverLocateIndexOffsetCallback? offset, }) { return innerJumpTo( index: index, sliverContext: sliverContext, isFixedHeight: isFixedHeight, alignment: alignment, padding: padding, offset: offset, renderSliverType: ObserverRenderSliverType.list, ); } } ================================================ FILE: lib/src/listview/list_observer_notification_result.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-08-12 20:07:18 */ import 'package:scrollview_observer/src/common/models/observer_handle_contexts_result_model.dart'; import 'package:scrollview_observer/src/common/observer_notification_result.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; import 'package:scrollview_observer/src/listview/models/listview_observe_model.dart'; class ListViewOnceObserveNotificationResult extends CommonOnceObserveNotificationResult> { ListViewOnceObserveNotificationResult({ required ObserverWidgetObserveResultType type, required ObserverHandleContextsResultModel observeResult, }) : super( type: type, observeResult: observeResult.changeResultModel, observeAllResult: observeResult.changeResultMap, ); } ================================================ FILE: lib/src/listview/list_observer_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/src/common/observer_typedef.dart'; import 'package:scrollview_observer/src/common/observer_widget.dart'; import 'package:scrollview_observer/src/listview/models/listview_observe_model.dart'; import 'package:scrollview_observer/src/notification.dart'; import 'package:scrollview_observer/src/observer_core.dart'; import 'list_observer_controller.dart'; class ListViewObserver extends ObserverWidget { /// The callback of getting all sliverList's buildContext. final List Function()? sliverListContexts; final ListObserverController? controller; const ListViewObserver({ Key? key, required Widget child, String? tag, this.controller, this.sliverListContexts, OnObserveAllCallback? onObserveAll, OnObserveCallback? onObserve, double leadingOffset = 0, double Function()? dynamicLeadingOffset, double toNextOverPercent = 1, ScrollNotificationPredicate? scrollNotificationPredicate, List? autoTriggerObserveTypes, ObserverTriggerOnObserveType triggerOnObserveType = ObserverTriggerOnObserveType.displayingItemsChange, ListViewObserveModel? Function(BuildContext context)? customHandleObserve, bool Function(RenderObject?)? customTargetRenderSliverType, }) : super( key: key, child: child, tag: tag, sliverController: controller, sliverContexts: sliverListContexts, onObserveAll: onObserveAll, onObserve: onObserve, leadingOffset: leadingOffset, dynamicLeadingOffset: dynamicLeadingOffset, toNextOverPercent: toNextOverPercent, scrollNotificationPredicate: scrollNotificationPredicate, autoTriggerObserveTypes: autoTriggerObserveTypes, triggerOnObserveType: triggerOnObserveType, customHandleObserve: customHandleObserve, customTargetRenderSliverType: customTargetRenderSliverType, ); @override State createState() => ListViewObserverState(); /// Returning the closest instance of this class that encloses the given /// context. /// /// If you give a tag, it will give priority find the corresponding instance /// of this class with the given tag and return it. /// /// If there is no [ListViewObserver] widget, then null is returned. /// /// Calling this method will create a dependency on the closest /// [ListViewObserver] in the [context], if there is one. /// /// See also: /// /// * [ListViewObserver.of], which is similar to this method, but asserts if no /// [ListViewObserver] instance is found. static ListViewObserverState? maybeOf( BuildContext context, { String? tag, }) { final _state = ObserverWidget.maybeOf< ListObserverController, ListViewObserveModel, ListViewOnceObserveNotification, ListViewObserver>( context, tag: tag, ); if (_state is! ListViewObserverState) return null; return _state; } /// Returning the closest instance of this class that encloses the given /// context. /// /// If you give a tag, it will give priority find the corresponding instance /// of this class with the given tag and return it. /// /// If no instance is found, this method will assert in debug mode, and throw /// an exception in release mode. /// /// Calling this method will create a dependency on the closest /// [ObserverWidget] in the [context]. /// /// See also: /// /// * [ObserverWidget.maybeOf], which is similar to this method, but returns /// null if no [ObserverWidget] instance is found. static ListViewObserverState of( BuildContext context, { String? tag, }) { final _state = ObserverWidget.of< ListObserverController, ListViewObserveModel, ListViewOnceObserveNotification, ListViewObserver>( context, tag: tag, ); return _state as ListViewObserverState; } /// Determine whether the [obj] is a supported RenderSliver type. static bool isSupportRenderSliverType(RenderObject? obj) { if (obj == null) return false; if (obj is RenderSliverList || obj is RenderSliverFixedExtentList) { return true; } final objRuntimeTypeStr = obj.runtimeType.toString(); final types = [ // New type added in flutter 3.16.0. // https://github.com/fluttercandies/flutter_scrollview_observer/issues/74 'RenderSliverVariedExtentList', ]; return types.contains(objRuntimeTypeStr); } } class ListViewObserverState extends ObserverWidgetState { @override ListViewObserveModel? handleObserve(BuildContext ctx) { if (widget.customHandleObserve != null) { return widget.customHandleObserve?.call(ctx); } return ObserverCore.handleListObserve( context: ctx, fetchLeadingOffset: fetchLeadingOffset, toNextOverPercent: widget.toNextOverPercent, ); } /// Determine whether it is the type of the target sliver. @override bool isTargetSliverContextType(RenderObject? obj) { if (widget.customTargetRenderSliverType != null) { return widget.customTargetRenderSliverType!.call(obj); } return ListViewObserver.isSupportRenderSliverType(obj); } } ================================================ FILE: lib/src/listview/models/listview_observe_displaying_child_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-07-03 15:46:45 */ import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/src/common/models/observe_displaying_child_model.dart'; import 'package:scrollview_observer/src/common/models/observe_displaying_child_model_mixin.dart'; class ListViewObserveDisplayingChildModel extends ObserveDisplayingChildModel with ObserveDisplayingChildModelMixin { ListViewObserveDisplayingChildModel({ required this.sliverList, required RenderViewportBase viewport, required int index, required RenderBox renderObject, }) : super( sliver: sliverList, viewport: viewport, index: index, renderObject: renderObject, ); /// The target sliverList. /// It would be [RenderSliverList] or [RenderSliverFixedExtentList]. RenderSliverMultiBoxAdaptor sliverList; @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is ListViewObserveDisplayingChildModel) { return index == other.index && renderObject == other.renderObject; } else { return false; } } @override int get hashCode { return index + renderObject.hashCode; } } ================================================ FILE: lib/src/listview/models/listview_observe_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/src/common/models/observe_model.dart'; import 'listview_observe_displaying_child_model.dart'; class ListViewObserveModel extends ObserveModel { ListViewObserveModel({ required this.sliverList, required RenderViewportBase viewport, required this.firstChild, required this.displayingChildModelList, required this.displayingChildModelMap, required bool visible, }) : super( visible: visible, sliver: sliverList, viewport: viewport, innerDisplayingChildModelList: displayingChildModelList, innerDisplayingChildModelMap: displayingChildModelMap, ); /// The target sliverList. /// It would be [RenderSliverList] or [RenderSliverFixedExtentList]. RenderSliverMultiBoxAdaptor sliverList; /// The observing data of the first child widget that is displaying. final ListViewObserveDisplayingChildModel? firstChild; /// Stores observing model list of displaying children widgets. final List displayingChildModelList; /// Stores observing model map of displaying children widgets. final Map displayingChildModelMap; @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is ListViewObserveModel) { return firstChild == other.firstChild && listEquals( displayingChildModelList, other.displayingChildModelList) && mapEquals(displayingChildModelMap, other.displayingChildModelMap); } else { return false; } } @override int get hashCode { return firstChild.hashCode + displayingChildModelList.hashCode + displayingChildModelMap.hashCode; } } ================================================ FILE: lib/src/notification.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-05-28 12:37:41 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/src/common/observer_controller.dart'; class ScrollViewOnceObserveNotification extends Notification { /// Whether to return the observation result directly without comparing. final bool isForce; /// Whether to depend on the observe callback. /// /// If true, the observe callback will be called when the observation result /// come out. final bool isDependObserveCallback; ScrollViewOnceObserveNotification({ this.isForce = false, this.isDependObserveCallback = true, }); } /// The Notification for Triggering an ListView observation class ListViewOnceObserveNotification extends ScrollViewOnceObserveNotification { ListViewOnceObserveNotification({ bool isForce = false, bool isDependObserveCallback = true, }) : super( isForce: isForce, isDependObserveCallback: isDependObserveCallback, ); } /// The Notification for Triggering an GridView observation class GridViewOnceObserveNotification extends ScrollViewOnceObserveNotification { GridViewOnceObserveNotification({ bool isForce = false, bool isDependObserveCallback = true, }) : super( isForce: isForce, isDependObserveCallback: isDependObserveCallback, ); } /// A notification of scrolling task. /// /// Sequence: /// [ObserverScrollStartNotification] -> [ObserverScrollDecisionNotification] /// -> [ObserverScrollEndNotification]. class ObserverScrollNotification extends Notification { @override void dispatch(BuildContext? target) { bool isMounted = target?.mounted ?? false; if (!isMounted) { return; } super.dispatch(target); } } /// A notification that a scrolling task has started due to calling the jumpTo /// or animateTo method of [ObserverController]. class ObserverScrollStartNotification extends ObserverScrollNotification {} /// A notification that a scrolling task has interrupted due to calling the /// jumpTo or animateTo method of [ObserverController]. class ObserverScrollInterruptionNotification extends ObserverScrollNotification {} /// A notification that the data of the specified index item is determined /// during the execution of the scrolling task. class ObserverScrollDecisionNotification extends ObserverScrollNotification {} /// A notification that a scrolling task has stopped due to calling the jumpTo /// or animateTo method of [ObserverController]. class ObserverScrollEndNotification extends ObserverScrollNotification {} ================================================ FILE: lib/src/observer_core.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-05-20 15:38:28 */ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'gridview/models/gridview_observe_displaying_child_model.dart'; import 'gridview/models/gridview_observe_model.dart'; import 'listview/models/listview_observe_displaying_child_model.dart'; import 'listview/models/listview_observe_model.dart'; import 'utils/observer_utils.dart'; class ObserverCore { /// Handles observation logic of a sliver similar to [SliverList]. static ListViewObserveModel? handleListObserve({ required BuildContext context, double Function()? fetchLeadingOffset, double? Function(BuildContext)? customOverlap, double toNextOverPercent = 1, }) { var _obj = ObserverUtils.findRenderObject(context); if (_obj is! RenderSliverMultiBoxAdaptor) return null; final viewport = ObserverUtils.findViewport(_obj); if (viewport == null) return null; if (kDebugMode) { if (viewport.debugNeedsPaint) return null; } // The geometry.visible is not absolutely reliable. if (!(_obj.geometry?.visible ?? false) || _obj.constraints.remainingPaintExtent < 1e-10) { return ListViewObserveModel( sliverList: _obj, viewport: viewport, visible: false, firstChild: null, displayingChildModelList: [], displayingChildModelMap: {}, ); } final scrollDirection = _obj.constraints.axis; var firstChild = _obj.firstChild; if (firstChild == null) return null; final offset = fetchLeadingOffset?.call() ?? 0; final overlap = customOverlap?.call(context) ?? _obj.constraints.overlap; final rawScrollViewOffset = _obj.constraints.scrollOffset + overlap; var scrollViewOffset = rawScrollViewOffset + offset; var parentData = firstChild.parentData as SliverMultiBoxAdaptorParentData; var index = parentData.index ?? 0; // Whether the first child being displayed is not found. bool isNotFound = false; // Find out the first child which is displaying var targetFirstChild = firstChild; while (!ObserverUtils.isBelowOffsetWidgetInSliver( scrollViewOffset: scrollViewOffset, scrollDirection: scrollDirection, targetChild: targetFirstChild, toNextOverPercent: toNextOverPercent, )) { index = index + 1; var nextChild = _obj.childAfter(targetFirstChild); if (nextChild == null) { isNotFound = true; break; } if (nextChild is! RenderIndexedSemantics) { // It is separator nextChild = _obj.childAfter(nextChild); } if (nextChild == null) { isNotFound = true; break; } targetFirstChild = nextChild; } // The first child being displayed is not found, indicating that the // ScrollView is not visible. if (isNotFound) { return ListViewObserveModel( sliverList: _obj, viewport: viewport, visible: false, firstChild: null, displayingChildModelList: [], displayingChildModelMap: {}, ); } if (targetFirstChild is! RenderIndexedSemantics) return null; final firstDisplayingChildIndex = targetFirstChild.index; final firstDisplayingChildModel = ListViewObserveDisplayingChildModel( sliverList: _obj, viewport: viewport, index: firstDisplayingChildIndex, renderObject: targetFirstChild, ); Map displayingChildModelMap = { firstDisplayingChildIndex: firstDisplayingChildModel, }; List displayingChildModelList = [ firstDisplayingChildModel, ]; // Find the remaining children that are being displayed final showingChildrenMaxOffset = rawScrollViewOffset + _obj.constraints.remainingPaintExtent - overlap; var displayingChild = _obj.childAfter(targetFirstChild); while (ObserverUtils.isDisplayingChildInSliver( targetChild: displayingChild, showingChildrenMaxOffset: showingChildrenMaxOffset, scrollViewOffset: scrollViewOffset, scrollDirection: scrollDirection, toNextOverPercent: toNextOverPercent, )) { if (displayingChild == null) { break; } if (displayingChild is! RenderIndexedSemantics) { // It is separator displayingChild = _obj.childAfter(displayingChild); continue; } final displayingChildIndex = displayingChild.index; final displayingChildModel = ListViewObserveDisplayingChildModel( sliverList: _obj, viewport: viewport, index: displayingChildIndex, renderObject: displayingChild, ); displayingChildModelList.add(displayingChildModel); displayingChildModelMap[displayingChildIndex] = displayingChildModel; displayingChild = _obj.childAfter(displayingChild); } return ListViewObserveModel( sliverList: _obj, viewport: viewport, visible: true, firstChild: firstDisplayingChildModel, displayingChildModelList: displayingChildModelList, displayingChildModelMap: displayingChildModelMap, ); } /// Handles observation logic of a sliver similar to [SliverGrid]. static GridViewObserveModel? handleGridObserve({ required BuildContext context, double Function()? fetchLeadingOffset, double? Function(BuildContext)? customOverlap, double toNextOverPercent = 1, }) { final _obj = ObserverUtils.findRenderObject(context); if (_obj is! RenderSliverMultiBoxAdaptor) return null; final viewport = ObserverUtils.findViewport(_obj); if (viewport == null) return null; if (kDebugMode) { if (viewport.debugNeedsPaint) return null; } // The geometry.visible is not absolutely reliable. if (!(_obj.geometry?.visible ?? false) || _obj.constraints.remainingPaintExtent < 1e-10) { return GridViewObserveModel( sliverGrid: _obj, viewport: viewport, visible: false, firstGroupChildList: [], displayingChildModelList: [], displayingChildModelMap: {}, ); } final scrollDirection = _obj.constraints.axis; var firstChild = _obj.firstChild; if (firstChild == null) return null; final offset = fetchLeadingOffset?.call() ?? 0; final overlap = customOverlap?.call(context) ?? _obj.constraints.overlap; final rawScrollViewOffset = _obj.constraints.scrollOffset + overlap; var scrollViewOffset = rawScrollViewOffset + offset; // Whether the first child being displayed is not found. bool isNotFound = false; // Find out the first child which is displaying var targetFirstChild = firstChild; var lastFirstGroupChildWidget = targetFirstChild; while (!ObserverUtils.isBelowOffsetWidgetInSliver( scrollViewOffset: scrollViewOffset, scrollDirection: scrollDirection, targetChild: targetFirstChild, toNextOverPercent: toNextOverPercent, )) { /// Entering here means it is not the target object RenderBox? nextChild = _obj.childAfter(targetFirstChild); if (nextChild == null) { isNotFound = true; break; } targetFirstChild = nextChild; } // The first child being displayed is not found, indicating that the // ScrollView is not visible. if (isNotFound) { return GridViewObserveModel( sliverGrid: _obj, viewport: viewport, visible: false, firstGroupChildList: [], displayingChildModelList: [], displayingChildModelMap: {}, ); } if (targetFirstChild is! RenderIndexedSemantics) return null; lastFirstGroupChildWidget = targetFirstChild; final firstDisplayingChildIndex = targetFirstChild.index; final firstModel = GridViewObserveDisplayingChildModel( sliverGrid: _obj, viewport: viewport, index: firstDisplayingChildIndex, renderObject: targetFirstChild, ); Map displayingChildModelMap = { firstDisplayingChildIndex: firstModel, }; List firstGroupChildModelList = [ firstModel, ]; final showingChildrenMaxOffset = rawScrollViewOffset + _obj.constraints.remainingPaintExtent - overlap; // Find out other child those have reached the specified offset. RenderBox? targetChild = _obj.childAfter(targetFirstChild); while (targetChild != null) { if (ObserverUtils.isReachOffsetWidgetInSliver( scrollViewOffset: max(scrollViewOffset, firstModel.layoutOffset), scrollDirection: scrollDirection, targetChild: targetChild, toNextOverPercent: toNextOverPercent, )) { if (targetChild is! RenderIndexedSemantics) break; final targetChildIndex = targetChild.index; final displayingChildModel = GridViewObserveDisplayingChildModel( sliverGrid: _obj, viewport: viewport, index: targetChildIndex, renderObject: targetChild, ); firstGroupChildModelList.add(displayingChildModel); displayingChildModelMap[targetChildIndex] = displayingChildModel; lastFirstGroupChildWidget = targetChild; } RenderBox? nextChild = _obj.childAfter(targetChild); if (nextChild == null) break; targetChild = nextChild; } List showingChildModelList = List.from(firstGroupChildModelList); // Find the remaining children that are being displayed var displayingChild = _obj.childAfter(lastFirstGroupChildWidget); while (displayingChild != null) { if (ObserverUtils.isDisplayingChildInSliver( targetChild: displayingChild, showingChildrenMaxOffset: showingChildrenMaxOffset, scrollViewOffset: scrollViewOffset, scrollDirection: scrollDirection, toNextOverPercent: toNextOverPercent, )) { if (displayingChild is! RenderIndexedSemantics) { continue; } final displayingChildIndex = displayingChild.index; final displayingChildModel = GridViewObserveDisplayingChildModel( sliverGrid: _obj, viewport: viewport, index: displayingChildIndex, renderObject: displayingChild, ); showingChildModelList.add(displayingChildModel); displayingChildModelMap[displayingChildIndex] = displayingChildModel; } displayingChild = _obj.childAfter(displayingChild); } return GridViewObserveModel( sliverGrid: _obj, viewport: viewport, visible: true, firstGroupChildList: firstGroupChildModelList, displayingChildModelList: showingChildModelList, displayingChildModelMap: displayingChildModelMap, ); } } ================================================ FILE: lib/src/sliver/models/sliver_observer_observe_result_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-08-12 16:18:26 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/src/common/models/observe_model.dart'; import 'package:scrollview_observer/src/common/models/observer_handle_contexts_result_model.dart'; import 'package:scrollview_observer/src/sliver/models/sliver_viewport_observe_model.dart'; class SliverObserverHandleContextsResultModel extends ObserverHandleContextsResultModel { /// Getting all slivers those are displayed in viewport. /// /// Corresponding to [onObserveViewport] in [SliverViewObserver]. final SliverViewportObserveModel? observeViewportResultModel; SliverObserverHandleContextsResultModel({ M? changeResultModel, Map changeResultMap = const {}, this.observeViewportResultModel, }) : super( changeResultModel: changeResultModel, changeResultMap: changeResultMap, ); } ================================================ FILE: lib/src/sliver/models/sliver_viewport_observe_displaying_child_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-05-14 10:51:42 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class SliverViewportObserveDisplayingChildModel { /// The [BuildContext] object for the [sliver]. final BuildContext sliverContext; /// The [sliver] displayed in the current CustomScrollView. final RenderSliver sliver; SliverViewportObserveDisplayingChildModel({ required this.sliverContext, required this.sliver, }); @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is SliverViewportObserveDisplayingChildModel) { return sliverContext == other.sliverContext && sliver == other.sliver; } else { return false; } } @override int get hashCode { return sliverContext.hashCode + sliver.hashCode; } } ================================================ FILE: lib/src/sliver/models/sliver_viewport_observe_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-05-13 22:36:22 */ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'sliver_viewport_observe_displaying_child_model.dart'; class SliverViewportObserveModel { /// The viewport of the current CustomScrollView. final RenderViewportBase viewport; /// The observing data of the first child widget that is displaying. final SliverViewportObserveDisplayingChildModel firstChild; /// Stores observing model list of displaying children widgets. final List displayingChildModelList; SliverViewportObserveModel({ required this.viewport, required this.firstChild, required this.displayingChildModelList, }); @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is SliverViewportObserveModel) { return viewport == other.viewport && firstChild == other.firstChild && listEquals(displayingChildModelList, other.displayingChildModelList); } else { return false; } } @override int get hashCode { return viewport.hashCode + firstChild.hashCode + displayingChildModelList.hashCode; } } ================================================ FILE: lib/src/sliver/sliver_observer_controller.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/observer_controller.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; import 'package:scrollview_observer/src/sliver/models/sliver_observer_observe_result_model.dart'; class SliverObserverController extends ObserverController with ObserverControllerForInfo, ObserverControllerForScroll, ObserverControllerForNotification< ObserveModel, SliverObserverHandleContextsResultModel, ScrollViewOnceObserveNotificationResult> { /// Whether to forbid the onObserveViewport callback. bool isForbidObserveViewportCallback = false; SliverObserverController({ ScrollController? controller, }) : super(controller: controller); /// Dispatch a [ScrollViewOnceObserveNotification] Future dispatchOnceObserve({ required BuildContext sliverContext, bool isForce = false, bool isDependObserveCallback = true, }) { return innerDispatchOnceObserve( sliverContext: sliverContext, notification: ScrollViewOnceObserveNotification( isForce: isForce, isDependObserveCallback: isDependObserveCallback, ), ); } /// Create a observation notification result. @override ScrollViewOnceObserveNotificationResult innerCreateOnceObserveNotificationResult({ required ObserverWidgetObserveResultType resultType, required SliverObserverHandleContextsResultModel? resultModel, }) { return ScrollViewOnceObserveNotificationResult( type: resultType, observeResult: resultModel ?? SliverObserverHandleContextsResultModel(), ); } /// Jump to the specified index position with animation. /// /// If the height of the child widget and the height of the separator are /// fixed, please pass the [isFixedHeight] parameter and the /// [renderSliverType] parameter . /// /// The [alignment] specifies the desired position for the leading edge of the /// child widget. It must be a value in the range [0.0, 1.0]. Future animateTo({ required int index, required Duration duration, required Curve curve, EdgeInsets padding = EdgeInsets.zero, BuildContext? sliverContext, bool isFixedHeight = false, double alignment = 0, ObserverLocateIndexOffsetCallback? offset, ObserverRenderSliverType? renderSliverType, ObserverOnPrepareScrollToIndex? onPrepareScrollToIndex, }) { return innerAnimateTo( index: index, duration: duration, curve: curve, padding: padding, sliverContext: sliverContext, isFixedHeight: isFixedHeight, alignment: alignment, offset: offset, renderSliverType: renderSliverType, onPrepareScrollToIndex: onPrepareScrollToIndex, ); } /// Jump to the specified index position without animation. /// /// If the height of the child widget and the height of the separator are /// fixed, please pass the [isFixedHeight] parameter and the /// [renderSliverType] parameter. /// /// If you do not pass the [isFixedHeight] parameter, the package will /// automatically gradually scroll around the target location before /// locating, which will produce an animation. /// /// The [renderSliverType] parameter is used to specify the type of sliver. /// If you do not pass the [renderSliverType] parameter, the sliding position /// will be calculated based on the actual type of obj, and there may be /// deviations in the calculation of elements for third-party libraries. /// /// The [alignment] specifies the desired position for the leading edge of the /// child widget. It must be a value in the range [0.0, 1.0]. Future jumpTo({ required int index, BuildContext? sliverContext, bool isFixedHeight = false, double alignment = 0, EdgeInsets padding = EdgeInsets.zero, ObserverLocateIndexOffsetCallback? offset, ObserverRenderSliverType? renderSliverType, ObserverOnPrepareScrollToIndex? onPrepareScrollToIndex, }) { return innerJumpTo( index: index, sliverContext: sliverContext, isFixedHeight: isFixedHeight, alignment: alignment, padding: padding, offset: offset, renderSliverType: renderSliverType, onPrepareScrollToIndex: onPrepareScrollToIndex, ); } } ================================================ FILE: lib/src/sliver/sliver_observer_listener.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-10-27 20:47:11 */ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:scrollview_observer/src/common/observer_typedef.dart'; class SliverObserverListenerEntry extends LinkedListEntry { SliverObserverListenerEntry({ required this.context, required this.onObserveViewport, }); /// The context of the listener. final BuildContext? context; /// The callback of getting all slivers those are displayed in viewport. final OnObserveViewportCallback? onObserveViewport; } ================================================ FILE: lib/src/sliver/sliver_observer_notification_result.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-08-12 20:13:21 */ import 'package:scrollview_observer/src/common/models/observe_model.dart'; import 'package:scrollview_observer/src/common/observer_notification_result.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; import 'package:scrollview_observer/src/sliver/models/sliver_observer_observe_result_model.dart'; import 'package:scrollview_observer/src/sliver/models/sliver_viewport_observe_model.dart'; class ScrollViewOnceObserveNotificationResult extends CommonOnceObserveNotificationResult> { ScrollViewOnceObserveNotificationResult({ required ObserverWidgetObserveResultType type, required SliverObserverHandleContextsResultModel observeResult, }) : super( type: type, observeResult: observeResult.changeResultModel, observeAllResult: observeResult.changeResultMap, ) { observeViewportResultModel = observeResult.observeViewportResultModel; } /// Getting all slivers those are displayed in viewport. /// /// Corresponding to [onObserveViewport] in [SliverViewObserver]. SliverViewportObserveModel? observeViewportResultModel; } ================================================ FILE: lib/src/sliver/sliver_observer_view.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-08 00:20:03 */ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/src/common/models/observe_model.dart'; import 'package:scrollview_observer/src/common/observer_typedef.dart'; import 'package:scrollview_observer/src/common/observer_widget.dart'; import 'package:scrollview_observer/src/listview/list_observer_view.dart'; import 'package:scrollview_observer/src/notification.dart'; import 'package:scrollview_observer/src/observer_core.dart'; import 'package:scrollview_observer/src/sliver/models/sliver_observer_observe_result_model.dart'; import 'package:scrollview_observer/src/sliver/models/sliver_viewport_observe_displaying_child_model.dart'; import 'package:scrollview_observer/src/sliver/sliver_observer_listener.dart'; import 'package:scrollview_observer/src/utils/observer_utils.dart'; import 'models/sliver_viewport_observe_model.dart'; import 'sliver_observer_controller.dart'; class SliverViewObserver extends ObserverWidget { /// The callback of getting all slivers those are displayed in viewport. final OnObserveViewportCallback? onObserveViewport; /// It's used to handle the observation logic for other types of Sliver /// besides [RenderSliverList], [RenderSliverFixedExtentList] and /// [RenderSliverGrid]. final ObserveModel? Function(BuildContext context)? extendedHandleObserve; /// The callback that specifies a custom overlap corresponds to sliverContext. /// /// If null is returned then use the overlap of sliverContext. final double? Function(BuildContext sliverContext)? customOverlap; final SliverObserverController? controller; const SliverViewObserver({ Key? key, required Widget child, String? tag, this.controller, @Deprecated( 'It will be removed in version 2, please use [sliverContexts] instead') List Function()? sliverListContexts, List Function()? sliverContexts, OnObserveAllCallback? onObserveAll, OnObserveCallback? onObserve, this.onObserveViewport, double leadingOffset = 0, double Function()? dynamicLeadingOffset, this.customOverlap, double toNextOverPercent = 1, ScrollNotificationPredicate? scrollNotificationPredicate, List? autoTriggerObserveTypes, ObserverTriggerOnObserveType triggerOnObserveType = ObserverTriggerOnObserveType.displayingItemsChange, ObserveModel? Function(BuildContext context)? customHandleObserve, this.extendedHandleObserve, }) : super( key: key, child: child, tag: tag, sliverController: controller, sliverContexts: sliverContexts ?? sliverListContexts, onObserveAll: onObserveAll, onObserve: onObserve, leadingOffset: leadingOffset, dynamicLeadingOffset: dynamicLeadingOffset, toNextOverPercent: toNextOverPercent, scrollNotificationPredicate: scrollNotificationPredicate, autoTriggerObserveTypes: autoTriggerObserveTypes, triggerOnObserveType: triggerOnObserveType, customHandleObserve: customHandleObserve, ); @override State createState() => MixViewObserverState(); /// Returning the closest instance of this class that encloses the given /// context. /// /// If you give a tag, it will give priority find the corresponding instance /// of this class with the given tag and return it. /// /// If there is no [SliverViewObserver] widget, then null is returned. /// /// Calling this method will create a dependency on the closest /// [SliverViewObserver] in the [context], if there is one. /// /// See also: /// /// * [SliverViewObserver.of], which is similar to this method, but asserts /// if no [SliverViewObserver] instance is found. static MixViewObserverState? maybeOf( BuildContext context, { String? tag, }) { final _state = ObserverWidget.maybeOf( context, tag: tag, ); if (_state is! MixViewObserverState) return null; return _state; } /// Returning the closest instance of this class that encloses the given /// context. /// /// If you give a tag, it will give priority find the corresponding instance /// of this class with the given tag and return it. /// /// If no instance is found, this method will assert in debug mode, and throw /// an exception in release mode. /// /// Calling this method will create a dependency on the closest /// [ObserverWidget] in the [context]. /// /// See also: /// /// * [ObserverWidget.maybeOf], which is similar to this method, but returns /// null if no [ObserverWidget] instance is found. static MixViewObserverState of( BuildContext context, { String? tag, }) { final _state = ObserverWidget.of( context, tag: tag, ); return _state as MixViewObserverState; } } class MixViewObserverState extends ObserverWidgetState { /// The last viewport observation result. SliverViewportObserveModel? lastViewportObserveResultModel; /// The listener list state for a [SliverViewObserver] returned by /// [SliverViewObserver.of]. /// /// It supports a listener list instead of just a single observation /// callback (such as onObserveViewport). @protected @visibleForTesting LinkedList? innerSliverListeners = LinkedList(); bool _debugAssertNotDisposed() { assert(() { if (innerSliverListeners == null) { throw FlutterError( 'A $runtimeType was used after being disposed.\n' 'Once you have called dispose() on a $runtimeType, it can no longer be used.', ); } return true; }()); return true; } @override void dispose() { assert(_debugAssertNotDisposed()); innerSliverListeners?.clear(); innerSliverListeners = null; super.dispose(); } @override SliverObserverHandleContextsResultModel? handleContexts({ bool isForceObserve = false, bool isFromObserveNotification = false, bool isDependObserveCallback = true, bool isIgnoreInnerCanHandleObserve = true, }) { if (!isIgnoreInnerCanHandleObserve) { if (!innerCanHandleObserve) return null; updateInnerCanHandleObserve(); } // Viewport final observeViewportResult = handleObserveViewport( isForceObserve: isForceObserve, isDependObserveCallback: isDependObserveCallback, ); _notifySliverListeners(observeViewportResult); // Slivers(SliverList, GridView etc.) final handleContextsResult = super.handleContexts( isForceObserve: isForceObserve, isFromObserveNotification: isFromObserveNotification, isDependObserveCallback: isDependObserveCallback, // It has been processed by the currently rewritten handleContexts method isIgnoreInnerCanHandleObserve: true, ); if (observeViewportResult == null && handleContextsResult == null) { return null; } return SliverObserverHandleContextsResultModel( changeResultModel: handleContextsResult?.changeResultModel, changeResultMap: handleContextsResult?.changeResultMap ?? {}, observeViewportResultModel: observeViewportResult, ); } @override ObserveModel? handleObserve(BuildContext ctx) { if (widget.customHandleObserve != null) { return widget.customHandleObserve?.call(ctx); } final _obj = ObserverUtils.findRenderObject(ctx); if (ListViewObserver.isSupportRenderSliverType(_obj)) { return ObserverCore.handleListObserve( context: ctx, fetchLeadingOffset: fetchLeadingOffset, customOverlap: widget.customOverlap, toNextOverPercent: widget.toNextOverPercent, ); } else if (_obj is RenderSliverGrid) { return ObserverCore.handleGridObserve( context: ctx, fetchLeadingOffset: fetchLeadingOffset, customOverlap: widget.customOverlap, toNextOverPercent: widget.toNextOverPercent, ); } return widget.extendedHandleObserve?.call(ctx); } /// To observe the viewport. SliverViewportObserveModel? handleObserveViewport({ bool isForceObserve = false, bool isDependObserveCallback = true, }) { final isForbidObserveViewportCallback = widget.sliverController?.isForbidObserveViewportCallback ?? false; final onObserveViewport = isForbidObserveViewportCallback ? null : widget.onObserveViewport; if (isDependObserveCallback && onObserveViewport == null && (innerSliverListeners?.isEmpty ?? true)) { return null; } final isHandlingScroll = widget.sliverController?.innerIsHandlingScroll ?? false; if (isHandlingScroll) return null; final ctxs = fetchTargetSliverContexts(); final objList = ctxs.map((e) => ObserverUtils.findRenderObject(e)).toList(); if (objList.isEmpty) return null; final firstObj = objList.first; if (firstObj == null) return null; final viewport = ObserverUtils.findViewport(firstObj); if (viewport == null) return null; final viewportOffset = viewport.offset; if (viewportOffset is! ScrollPosition) return null; var targetChild = viewport.firstChild; if (targetChild == null) return null; var offset = widget.leadingOffset; if (widget.dynamicLeadingOffset != null) { offset = widget.dynamicLeadingOffset!(); } final pixels = viewportOffset.pixels; final startCalcPixels = pixels + offset; int indexOfTargetChild = objList.indexOf(targetChild); // Find out the first sliver which is displayed in viewport. final dimension = viewportOffset.viewportDimension; final viewportBottomOffset = pixels + dimension; while (!ObserverUtils.isValidListIndex(indexOfTargetChild) || !ObserverUtils.isDisplayingSliverInViewport( sliver: targetChild, viewportPixels: startCalcPixels, viewportBottomOffset: viewportBottomOffset, )) { if (targetChild == null) break; final nextChild = viewport.childAfter(targetChild); if (nextChild == null) break; targetChild = nextChild; indexOfTargetChild = objList.indexOf(targetChild); } if (targetChild == null || !ObserverUtils.isValidListIndex(indexOfTargetChild)) { return null; } final targetCtx = ctxs[indexOfTargetChild]; final firstChild = SliverViewportObserveDisplayingChildModel( sliverContext: targetCtx, sliver: targetChild, ); List displayingChildModelList = [ firstChild ]; // Find the remaining children that are being displayed. targetChild = viewport.childAfter(targetChild); while (targetChild != null) { // The current targetChild is not displayed, so the later children don't // need to be check if (!ObserverUtils.isDisplayingSliverInViewport( sliver: targetChild, viewportPixels: startCalcPixels, viewportBottomOffset: viewportBottomOffset, )) { break; } indexOfTargetChild = objList.indexOf(targetChild); if (ObserverUtils.isValidListIndex(indexOfTargetChild)) { // The current targetChild is target. final context = ctxs[indexOfTargetChild]; displayingChildModelList.add(SliverViewportObserveDisplayingChildModel( sliverContext: context, sliver: targetChild, )); } // continue to check next child. targetChild = viewport.childAfter(targetChild); } var model = SliverViewportObserveModel( viewport: viewport, firstChild: firstChild, displayingChildModelList: displayingChildModelList, ); bool canReturnResult = false; if (isForceObserve || widget.triggerOnObserveType == ObserverTriggerOnObserveType.directly) { canReturnResult = true; } else if (model != lastViewportObserveResultModel) { canReturnResult = true; } if (canReturnResult && isDependObserveCallback && onObserveViewport != null) { onObserveViewport(model); } // Record it for the next comparison. lastViewportObserveResultModel = model; return canReturnResult ? model : null; } @override void addListener({ BuildContext? context, OnObserveCallback? onObserve, OnObserveAllCallback? onObserveAll, OnObserveViewportCallback? onObserveViewport, }) { assert(_debugAssertNotDisposed()); assert( onObserve != null || onObserveAll != null || onObserveViewport != null, 'At least one callback must be provided.', ); super.addListener( context: context, onObserve: onObserve, onObserveAll: onObserveAll, ); // Add the listener for the viewport observation. if (onObserveViewport != null) { innerSliverListeners?.add(SliverObserverListenerEntry( context: context, onObserveViewport: onObserveViewport, )); } } @override void removeListener({ BuildContext? context, OnObserveCallback? onObserve, OnObserveAllCallback? onObserveAll, OnObserveViewportCallback? onObserveViewport, }) { assert(_debugAssertNotDisposed()); assert( onObserve != null || onObserveAll != null || onObserveViewport != null, 'At least one callback must be provided.', ); super.removeListener( context: context, onObserve: onObserve, onObserveAll: onObserveAll, ); // Remove the listener for the viewport observation. final listeners = innerSliverListeners; if (listeners == null) return; for (final SliverObserverListenerEntry entry in listeners) { if (entry.context == context && entry.onObserveViewport == onObserveViewport) { entry.unlink(); return; } } } void _notifySliverListeners( SliverViewportObserveModel? observeViewportResult, ) { if (observeViewportResult == null) return; final listeners = innerSliverListeners; if (listeners == null || listeners.isEmpty) return; final List localListeners = List.of(listeners); for (final SliverObserverListenerEntry entry in localListeners) { try { if (entry.list != null) { entry.onObserveViewport?.call(observeViewportResult); } } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'scrollview_observer', context: ErrorDescription('while dispatching result for $runtimeType'), informationCollector: () => [ DiagnosticsProperty( 'The $runtimeType sending result was', this, style: DiagnosticsTreeStyle.errorProperty, ), ], )); } } } } ================================================ FILE: lib/src/utils/observer_utils.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-21 01:07:16 */ export 'src/extends.dart'; export 'src/slivers.dart'; export 'src/observer_utils.dart'; export 'src/nested_scroll_util.dart'; export 'src/chat/chat_observer_scroll_physics.dart'; export 'src/chat/chat_observer_scroll_physics_mixin.dart'; export 'src/chat/chat_scroll_observer.dart'; export 'src/chat/chat_scroll_observer_typedefs.dart'; export 'src/chat/chat_scroll_observer_model.dart'; ================================================ FILE: lib/src/utils/src/chat/chat_observer_scroll_physics.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-09-27 23:12:45 */ import 'package:flutter/material.dart'; import 'chat_observer_scroll_physics_mixin.dart'; import 'chat_scroll_observer.dart'; @Deprecated( 'It will be removed in version 2, please use [ChatObserverClampingScrollPhysics] instead') class ChatObserverClampinScrollPhysics extends ChatObserverClampingScrollPhysics { ChatObserverClampinScrollPhysics({ required ChatScrollObserver observer, }) : super(observer: observer); } class ChatObserverClampingScrollPhysics extends ClampingScrollPhysics with ChatObserverScrollPhysicsMixin { ChatObserverClampingScrollPhysics({ ScrollPhysics? parent, required ChatScrollObserver observer, }) : super(parent: parent) { this.observer = observer; } @override ChatObserverClampingScrollPhysics applyTo(ScrollPhysics? ancestor) { return ChatObserverClampingScrollPhysics( parent: buildParent(ancestor), observer: observer, ); } } class ChatObserverBouncingScrollPhysics extends BouncingScrollPhysics with ChatObserverScrollPhysicsMixin { ChatObserverBouncingScrollPhysics({ ScrollPhysics? parent, required ChatScrollObserver observer, }) : super(parent: parent) { this.observer = observer; } @override ChatObserverBouncingScrollPhysics applyTo(ScrollPhysics? ancestor) { return ChatObserverBouncingScrollPhysics( parent: buildParent(ancestor), observer: observer, ); } } ================================================ FILE: lib/src/utils/src/chat/chat_observer_scroll_physics_mixin.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-09-29 22:47:10 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; mixin ChatObserverScrollPhysicsMixin on ScrollPhysics { late final ChatScrollObserver observer; @override double adjustPositionForNewDimensions({ required ScrollMetrics oldPosition, required ScrollMetrics newPosition, required bool isScrolling, required double velocity, }) { final isNeedFixedPosition = observer.innerIsNeedFixedPosition; observer.innerIsNeedFixedPosition = false; var adjustPosition = super.adjustPositionForNewDimensions( oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity, ); if (newPosition.extentBefore <= observer.fixedPositionOffset || !isNeedFixedPosition || observer.isRemove) { _handlePositionCallback(ChatScrollObserverHandlePositionResultModel( type: ChatScrollObserverHandlePositionType.none, mode: observer.innerMode, changeCount: observer.changeCount, )); return adjustPosition; } // Customize the adjustPosition. double? customAdjustPosition = observer.customAdjustPosition?.call( ChatScrollObserverCustomAdjustPositionModel( oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity, adjustPosition: adjustPosition, observer: observer, ), ); if (customAdjustPosition != null) { _handlePositionCallback(ChatScrollObserverHandlePositionResultModel( type: ChatScrollObserverHandlePositionType.keepPosition, mode: observer.innerMode, changeCount: observer.changeCount, )); return customAdjustPosition; } final model = observer.observeRefItem(); if (model == null) { _handlePositionCallback(ChatScrollObserverHandlePositionResultModel( type: ChatScrollObserverHandlePositionType.none, mode: observer.innerMode, changeCount: observer.changeCount, )); return adjustPosition; } _handlePositionCallback(ChatScrollObserverHandlePositionResultModel( type: ChatScrollObserverHandlePositionType.keepPosition, mode: observer.innerMode, changeCount: observer.changeCount, )); // Customize the delta of the adjustPosition. double? customDelta = observer.customAdjustPositionDelta?.call( ChatScrollObserverCustomAdjustPositionDeltaModel( oldPosition: oldPosition, newPosition: newPosition, isScrolling: isScrolling, velocity: velocity, adjustPosition: adjustPosition, observer: observer, currentItemModel: model, ), ); // Calculate the final delta. // // If the customDelta is not null, use the customDelta. // Otherwise, use the layoutOffset minus innerRefItemLayoutOffset to get // the difference in the leading offset of the item. final delta = customDelta ?? (model.layoutOffset - observer.innerRefItemLayoutOffset); return adjustPosition + delta; } @override bool shouldAcceptUserOffset(ScrollMetrics position) => true; /// Calling observer's [onHandlePositionCallback]. _handlePositionCallback(ChatScrollObserverHandlePositionResultModel result) { ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((timeStamp) { observer.onHandlePositionResultCallback?.call(result); // ignore: deprecated_member_use_from_same_package observer.onHandlePositionCallback?.call(result.type); }); } } ================================================ FILE: lib/src/utils/src/chat/chat_scroll_observer.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-09-27 23:01:58 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; class ChatScrollObserver { ChatScrollObserver(this.observerController) { // Ensure isShrinkWrap is correct at the end of this frame. WidgetsBinding.instance.addPostFrameCallback((timeStamp) { observeSwitchShrinkWrap(); }); } /// Used to obtain the necessary child widget information. final ListObserverController observerController; /// Whether a fixed position is required. bool get isNeedFixedPosition => innerIsNeedFixedPosition; bool innerIsNeedFixedPosition = false; /// The index of the reference. int get refItemIndex => innerRefItemIndex; int innerRefItemIndex = 0; /// The index of the reference after ScrollView children update. int get refItemIndexAfterUpdate => innerRefItemIndexAfterUpdate; int innerRefItemIndexAfterUpdate = 0; /// The [layoutOffset] of the reference. double get refItemLayoutOffset => innerRefItemLayoutOffset; double innerRefItemLayoutOffset = 0; /// Control the [shrinkWrap] properties of the external scroll view. bool get isShrinkWrap => innerIsShrinkWrap; bool innerIsShrinkWrap = true; /// Whether is remove chat data. bool isRemove = false; /// The number of messages added. int changeCount = 1; /// The current chat location is retained when the scrollView offset is /// greater than [fixedPositionOffset]. double fixedPositionOffset = 0; /// The callback that tells the outside to rebuild the scroll view. /// /// Such as call [setState] method. Function? toRebuildScrollViewCallback; /// The result callback for processing chat location. /// /// This callback will be called when handling in [ClampingScrollPhysics]'s /// [adjustPositionForNewDimensions]. @Deprecated( 'It will be removed in version 2, please use [onHandlePositionResultCallback] instead') void Function(ChatScrollObserverHandlePositionType)? onHandlePositionCallback; /// The result callback for processing chat location. /// /// This callback will be called when handling in [ClampingScrollPhysics]'s /// [adjustPositionForNewDimensions]. void Function(ChatScrollObserverHandlePositionResultModel)? onHandlePositionResultCallback; /// The mode of processing. ChatScrollObserverHandleMode innerMode = ChatScrollObserverHandleMode.normal; /// Customize the delta of the adjustPosition. /// /// If the return value is null, the default processing will be performed. ChatScrollObserverCustomAdjustPositionDelta? customAdjustPositionDelta; /// Customize the scroll position for new viewport dimensions. /// /// If the return value is null, the default processing will be performed. ChatScrollObserverCustomAdjustPosition? customAdjustPosition; /// Observation result of reference item after ScrollView children update. ListViewObserveDisplayingChildModel? observeRefItem() { return observerController.observeItem( index: refItemIndexAfterUpdate, ); } /// Prepare to adjust position for sliver. /// /// The [changeCount] parameter is used only when [isRemove] parameter is /// false. /// /// The [mode] parameter is used to specify the processing mode. /// /// [refItemRelativeIndex] parameter and [refItemRelativeIndexAfterUpdate] /// parameter are only used when the mode is /// [ChatScrollObserverHandleMode.specified]. /// Usage: When you insert a new message, assign the index of the reference /// message before insertion to [refItemIndex], and assign the index of the /// reference message after insertion to [refItemIndexAfterUpdate]. /// Note that they should refer to the index of the same message. standby({ BuildContext? sliverContext, bool isRemove = false, int changeCount = 1, ChatScrollObserverHandleMode mode = ChatScrollObserverHandleMode.normal, ChatScrollObserverRefIndexType refIndexType = ChatScrollObserverRefIndexType.relativeIndexStartFromCacheExtent, @Deprecated( 'It will be removed in version 2, please use [refItemIndex] instead') int refItemRelativeIndex = 0, @Deprecated( 'It will be removed in version 2, please use [refItemIndexAfterUpdate] instead') int refItemRelativeIndexAfterUpdate = 0, int refItemIndex = 0, int refItemIndexAfterUpdate = 0, ChatScrollObserverCustomAdjustPosition? customAdjustPosition, ChatScrollObserverCustomAdjustPositionDelta? customAdjustPositionDelta, }) async { innerMode = mode; this.isRemove = isRemove; this.changeCount = changeCount; observeSwitchShrinkWrap(); int _innerRefItemIndex; int _innerRefItemIndexAfterUpdate; double _innerRefItemLayoutOffset; switch (mode) { case ChatScrollObserverHandleMode.normal: final firstItemModel = observerController.observeFirstItem( sliverContext: sliverContext, ); if (firstItemModel == null) return; _innerRefItemIndex = firstItemModel.index; _innerRefItemIndexAfterUpdate = _innerRefItemIndex + changeCount; _innerRefItemLayoutOffset = firstItemModel.layoutOffset; break; case ChatScrollObserverHandleMode.generative: final firstItemModel = observerController.observeFirstItem( sliverContext: sliverContext, ); if (firstItemModel == null) return; int index = firstItemModel.index + changeCount; final model = observerController.observeItem( sliverContext: sliverContext, index: index, ); if (model == null) return; _innerRefItemIndex = index; _innerRefItemIndexAfterUpdate = index; _innerRefItemLayoutOffset = model.layoutOffset; break; case ChatScrollObserverHandleMode.specified: // Prioritize the values ​​of [refItemIndex] and [refItemIndexAfterUpdate] int _refItemIndex = refItemIndex != 0 ? refItemIndex : refItemRelativeIndex; int _refItemIndexAfterUpdate = refItemIndexAfterUpdate != 0 ? refItemIndexAfterUpdate : refItemRelativeIndexAfterUpdate; switch (refIndexType) { case ChatScrollObserverRefIndexType.relativeIndexStartFromCacheExtent: final firstItemModel = observerController.observeFirstItem( sliverContext: sliverContext, ); if (firstItemModel == null) return; int index = firstItemModel.index + _refItemIndex; final model = observerController.observeItem( sliverContext: sliverContext, index: index, ); if (model == null) return; _innerRefItemIndex = index; _innerRefItemIndexAfterUpdate = firstItemModel.index + _refItemIndexAfterUpdate; _innerRefItemLayoutOffset = model.layoutOffset; break; case ChatScrollObserverRefIndexType.relativeIndexStartFromDisplaying: final observeResult = await observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); if (!observeResult.isSuccess) return; final currentFirstDisplayingChildIndex = observeResult.observeResult?.firstChild?.index ?? 0; int index = currentFirstDisplayingChildIndex + _refItemIndex; final model = observerController.observeItem( sliverContext: sliverContext, index: index, ); if (model == null) return; _innerRefItemIndex = index; _innerRefItemIndexAfterUpdate = currentFirstDisplayingChildIndex + _refItemIndexAfterUpdate; _innerRefItemLayoutOffset = model.layoutOffset; break; case ChatScrollObserverRefIndexType.itemIndex: final model = observerController.observeItem( sliverContext: sliverContext, index: _refItemIndex, ); if (model == null) return; _innerRefItemIndex = _refItemIndex; _innerRefItemIndexAfterUpdate = _refItemIndexAfterUpdate; _innerRefItemLayoutOffset = model.layoutOffset; break; } } // Record value. innerIsNeedFixedPosition = true; innerRefItemIndex = _innerRefItemIndex; innerRefItemIndexAfterUpdate = _innerRefItemIndexAfterUpdate; innerRefItemLayoutOffset = _innerRefItemLayoutOffset; this.customAdjustPosition = customAdjustPosition; this.customAdjustPositionDelta = customAdjustPositionDelta; // When the heights of items are similar, the viewport will not call // [performLayout], In this case, the [adjustPositionForNewDimensions] of // [ScrollPhysics] will not be called, which makes the function of keeping // position invalid. // // So here let it record a layout-time correction to the scroll offset, and // call [markNeedsLayout] to prompt the viewport to be re-layout to solve // the above problem. // // Related issue // https://github.com/fluttercandies/flutter_scrollview_observer/issues/64 final ctx = observerController.fetchSliverContext(); if (ctx == null) return; final obj = ObserverUtils.findRenderObject(ctx); if (obj == null) return; final viewport = ObserverUtils.findViewport(obj); if (viewport == null) return; if (!viewport.offset.hasPixels) return; viewport.offset.correctBy(0); viewport.markNeedsLayout(); } observeSwitchShrinkWrap() { ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) { final ctx = observerController.fetchSliverContext(); if (ctx == null) return; final obj = ObserverUtils.findRenderObject(ctx); if (obj is! RenderSliver) return; final constraints = ObserverUtils.sliverConstraints(obj); if (constraints == null) return; final viewportMainAxisExtent = constraints.viewportMainAxisExtent; final scrollExtent = obj.geometry?.scrollExtent ?? 0; if (viewportMainAxisExtent >= scrollExtent) { if (innerIsShrinkWrap) return; innerIsShrinkWrap = true; observerController.reattach(); toRebuildScrollViewCallback?.call(); } else { if (!innerIsShrinkWrap) return; innerIsShrinkWrap = false; observerController.reattach(); toRebuildScrollViewCallback?.call(); } }); } } ================================================ FILE: lib/src/utils/src/chat/chat_scroll_observer_model.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2023-05-13 10:33:00 */ import 'package:flutter/material.dart'; import 'package:scrollview_observer/src/common/models/observe_displaying_child_model_mixin.dart'; import 'package:scrollview_observer/src/utils/src/chat/chat_scroll_observer.dart'; import 'package:scrollview_observer/src/utils/src/chat/chat_scroll_observer_typedefs.dart'; class ChatScrollObserverHandlePositionResultModel { /// The type of processing location. final ChatScrollObserverHandlePositionType type; /// The mode of processing. final ChatScrollObserverHandleMode mode; /// The number of messages added. final int changeCount; ChatScrollObserverHandlePositionResultModel({ required this.type, required this.mode, required this.changeCount, }); } class ChatScrollObserverCustomAdjustPositionDeltaModel { /// The old position. final ScrollMetrics oldPosition; /// The new position. final ScrollMetrics newPosition; /// Whether the ScrollView is scrolling. final bool isScrolling; /// The current velocity of the scroll position. final double velocity; /// The scroll position should be given for new viewport dimensions. final double adjustPosition; /// The [ChatScrollObserver] instance. final ChatScrollObserver observer; /// The observation result of the current item. final ObserveDisplayingChildModelMixin currentItemModel; ChatScrollObserverCustomAdjustPositionDeltaModel({ required this.oldPosition, required this.newPosition, required this.isScrolling, required this.velocity, required this.adjustPosition, required this.observer, required this.currentItemModel, }); } class ChatScrollObserverCustomAdjustPositionModel { /// The old position. final ScrollMetrics oldPosition; /// The new position. final ScrollMetrics newPosition; /// Whether the ScrollView is scrolling. final bool isScrolling; /// The current velocity of the scroll position. final double velocity; /// The scroll position should be given for new viewport dimensions. final double adjustPosition; /// The [ChatScrollObserver] instance. final ChatScrollObserver observer; ChatScrollObserverCustomAdjustPositionModel({ required this.oldPosition, required this.newPosition, required this.isScrolling, required this.velocity, required this.adjustPosition, required this.observer, }); } ================================================ FILE: lib/src/utils/src/chat/chat_scroll_observer_typedefs.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-10-31 14:57:45 */ import 'package:scrollview_observer/src/utils/src/chat/chat_scroll_observer_model.dart'; /// Customize the delta of the adjustPosition. typedef ChatScrollObserverCustomAdjustPositionDelta = double? Function( ChatScrollObserverCustomAdjustPositionDeltaModel, ); /// Customize the scroll position should be given new viewport dimensions. typedef ChatScrollObserverCustomAdjustPosition = double? Function( ChatScrollObserverCustomAdjustPositionModel, ); enum ChatScrollObserverHandlePositionType { /// Nothing will be done. none, /// Keep the current chat location. keepPosition, } enum ChatScrollObserverHandleMode { /// Regular mode /// Such as inserting or deleting messages. normal, /// Generative mode /// Such as ChatGPT streaming messages. generative, /// Specified mode /// You can specify the index of the reference message in this mode. specified, } enum ChatScrollObserverRefIndexType { /// relativeIndex trailing /// /// 6 | item16 | cacheExtent /// ----------------- ----------------- /// 5 | item15 | /// 4 | item14 | /// 3 | item13 | displaying /// 2 | item12 | /// 1 | item11 | /// ----------------- ----------------- /// 0 | item10 | cacheExtent <---- start /// /// leading relativeIndexStartFromCacheExtent, /// relativeIndex trailing /// /// 5 | item16 | cacheExtent /// ----------------- ----------------- /// 4 | item15 | /// 3 | item14 | /// 2 | item13 | displaying /// 1 | item12 | /// 0 | item11 | <---- start /// ----------------- ----------------- /// -1 | item10 | cacheExtent /// /// leading relativeIndexStartFromDisplaying, /// Directly specify the index of item. itemIndex, } ================================================ FILE: lib/src/utils/src/extends.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-03-12 22:50:53 */ import 'package:flutter/rendering.dart'; extension ObserverDouble on double { /// Rectify the value according to the current growthDirection of sliver. /// /// If the growthDirection is [GrowthDirection.forward], the value is /// returned directly, otherwise the opposite value is returned. double rectify( RenderSliver obj, ) { return obj.isForwardGrowthDirection ? this : -this; } } extension ObserverRenderSliverMultiBoxAdaptor on RenderSliver { /// Determine whether the current growthDirection of sliver is /// [GrowthDirection.forward]. bool get isForwardGrowthDirection { return GrowthDirection.forward == constraints.growthDirection; } } ================================================ FILE: lib/src/utils/src/log.dart ================================================ import 'dart:developer' as developer; class Log { Log._(); static void info(String msg) { _log('\x1B[34m$msg\x1B[0m'); } static void success(String msg) { _log('\x1B[32m$msg\x1B[0m'); } static warning(String msg) { _log('\x1B[33m$msg\x1B[0m'); } static error(String msg) { _log('\x1B[31m$msg\x1B[0m'); } static _log(String message) { developer.log(message, name: 'scrollview_observer'); } } ================================================ FILE: lib/src/utils/src/nested_scroll_util.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-12-04 20:15:33 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/typedefs.dart'; enum NestedScrollUtilPosition { /// Corresponds to the headerSliver in [NestedScrollView]. header, /// Corresponds to the body in [NestedScrollView]. body, } class NestedScrollUtil { /// Record the [BuildContext] corresponding to all header slivers of /// NestedScrollView. List headerSliverContexts = []; /// Record the [BuildContext] corresponding to all body slivers of /// NestedScrollView. List bodySliverContexts = []; /// Record the [BuildContext] of [SliverFillRemaining]. BuildContext? remainingSliverContext; /// Record the [RenderObject] of [SliverFillRemaining]. RenderSliverSingleBoxAdapter? remainingSliverRenderObj; /// The outer [ScrollController] of a NestedScrollView. ScrollController? outerScrollController; /// The inner [ScrollController] in the body of a NestedScrollView. ScrollController? bodyScrollController; /// Calculate the overlap for the body sliver. double? calcOverlap({ required GlobalKey nestedScrollViewKey, required BuildContext sliverContext, }) { final nestedScrollViewCtx = nestedScrollViewKey.currentContext; if (nestedScrollViewCtx == null) return null; // If the sliver of ctx is headerSliver, just return null and use the // default overlap. if (!bodySliverContexts.contains(sliverContext)) return null; // Get SliverFillRemaining final remainingSliverContext = fetchRemainingSliverContext( nestedScrollViewKey: nestedScrollViewKey, ); if (remainingSliverContext == null || remainingSliverRenderObj == null) { return null; } /// Calculate the offset of the sliver corresponding to sliverContext /// relative to SliverFillRemaining. final offset = ObserverUtils.localToGlobal( context: sliverContext, point: Offset.zero, ancestor: remainingSliverContext, ); if (offset == null) return null; final remainingContextOverlap = remainingSliverRenderObj!.constraints.overlap; final sliverContextExtraOverlap = (remainingContextOverlap - offset.dy).clamp(0, double.infinity); var _obj = ObserverUtils.findRenderObject(sliverContext); if (_obj is! RenderSliverMultiBoxAdaptor) return null; return sliverContextExtraOverlap + _obj.constraints.overlap; } /// Calculate the [precedingScrollExtent] for [sliverContext]. double? calcPrecedingScrollExtent({ required GlobalKey nestedScrollViewKey, required BuildContext sliverContext, }) { double precedingScrollExtent = 0; var _obj = ObserverUtils.findRenderObject(sliverContext); if (_obj is! RenderSliverMultiBoxAdaptor) return null; precedingScrollExtent = _obj.constraints.precedingScrollExtent; // Get SliverFillRemaining final remainingSliverContext = fetchRemainingSliverContext( nestedScrollViewKey: nestedScrollViewKey, ); if (remainingSliverContext == null || remainingSliverRenderObj == null) { return null; } precedingScrollExtent += remainingSliverRenderObj?.constraints.precedingScrollExtent ?? 0; return precedingScrollExtent; } /// Reset all data. reset() { headerSliverContexts.clear(); bodySliverContexts.clear(); remainingSliverContext = null; remainingSliverRenderObj = null; outerScrollController = null; bodyScrollController = null; } /// Get SliverFillRemaining BuildContext? fetchRemainingSliverContext({ required GlobalKey nestedScrollViewKey, }) { // Find out SliverFillRemaining final nestedScrollViewCtx = nestedScrollViewKey.currentContext; if (nestedScrollViewCtx == null) return null; remainingSliverContext ??= ObserverUtils.findChildContext( context: nestedScrollViewCtx, isTargetType: (ctx) { final obj = ctx.findRenderObject(); if (obj is RenderSliverSingleBoxAdapter) { remainingSliverRenderObj = obj; return true; } return false; }, ); return remainingSliverContext; } /// Jump to the specified index position. Future jumpTo({ required GlobalKey nestedScrollViewKey, required SliverObserverController observerController, required NestedScrollUtilPosition position, required int index, required BuildContext? sliverContext, bool isFixedHeight = false, double alignment = 0, EdgeInsets padding = EdgeInsets.zero, ObserverLocateIndexOffsetCallback? offset, ObserverRenderSliverType? renderSliverType, }) { assert(outerScrollController != null, 'outerScrollController is null'); assert(bodyScrollController != null, 'bodyScrollController is null'); if (outerScrollController == null) return Future.value(); if (bodyScrollController == null) return Future.value(); switchScrollController( observerController: observerController, position: position, ); return observerController.jumpTo( index: index, sliverContext: sliverContext, isFixedHeight: isFixedHeight, alignment: alignment, padding: padding, offset: offset, renderSliverType: renderSliverType, onPrepareScrollToIndex: handleOnPrepareScrollToIndex( nestedScrollViewKey: nestedScrollViewKey, position: position, outerScrollController: outerScrollController!, offset: offset, ), ); } /// Animate to the specified index position. Future animateTo({ required GlobalKey nestedScrollViewKey, required SliverObserverController observerController, required NestedScrollUtilPosition position, required int index, required Duration duration, required Curve curve, required BuildContext? sliverContext, bool isFixedHeight = false, double alignment = 0, EdgeInsets padding = EdgeInsets.zero, ObserverLocateIndexOffsetCallback? offset, ObserverRenderSliverType? renderSliverType, }) { assert(outerScrollController != null, 'outerScrollController is null'); assert(bodyScrollController != null, 'bodyScrollController is null'); if (outerScrollController == null) return Future.value(); if (bodyScrollController == null) return Future.value(); switchScrollController( observerController: observerController, position: position, ); return observerController.animateTo( index: index, duration: duration, curve: curve, sliverContext: sliverContext, isFixedHeight: isFixedHeight, alignment: alignment, padding: padding, offset: offset, renderSliverType: renderSliverType, onPrepareScrollToIndex: handleOnPrepareScrollToIndex( nestedScrollViewKey: nestedScrollViewKey, position: position, outerScrollController: outerScrollController!, duration: duration, curve: curve, offset: offset, ), ); } /// Switch the [ScrollController] of [observerController] according to the /// [NestedScrollUtilPosition]. switchScrollController({ required SliverObserverController observerController, required NestedScrollUtilPosition position, }) { assert(outerScrollController != null, 'outerScrollController is null'); assert(bodyScrollController != null, 'bodyScrollController is null'); if (outerScrollController == null) return; if (bodyScrollController == null) return; switch (position) { case NestedScrollUtilPosition.header: observerController.controller = outerScrollController; break; case NestedScrollUtilPosition.body: observerController.controller = bodyScrollController; break; } } /// Handle the [onPrepareScrollToIndex] callback. ObserverOnPrepareScrollToIndex? handleOnPrepareScrollToIndex({ required GlobalKey nestedScrollViewKey, required NestedScrollUtilPosition position, required ScrollController outerScrollController, Duration? duration, Curve? curve, ObserverLocateIndexOffsetCallback? offset, }) { switch (position) { case NestedScrollUtilPosition.header: return null; case NestedScrollUtilPosition.body: return (calcResult) async { if (calcResult.calculateTargetLayoutOffset > 0) { // Here we can get the item's offset accurately. return false; } // The item is located relatively top, and the accurate item // offset cannot be obtained. // So here we jump through outerScrollController. final remainingSliverContext = fetchRemainingSliverContext( nestedScrollViewKey: nestedScrollViewKey, ); var remainingSliverContextObj = ObserverUtils.findRenderObject( remainingSliverContext, ); if (remainingSliverContextObj is! RenderSliverSingleBoxAdapter) { return false; } double targetOffset = remainingSliverContextObj.constraints.precedingScrollExtent; targetOffset -= offset?.call(targetOffset) ?? 0; targetOffset += calcResult.targetChildLayoutOffset; if (duration != null && curve != null) { await outerScrollController.animateTo( targetOffset, duration: duration, curve: curve, ); } else { outerScrollController.jumpTo(targetOffset); } return true; }; } } } ================================================ FILE: lib/src/utils/src/observer_utils.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/LinXunFeng/flutter_scrollview_observer * @Date: 2022-08-21 00:53:44 */ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:scrollview_observer/src/common/models/observe_model.dart'; import 'package:scrollview_observer/src/gridview/models/gridview_observe_displaying_child_model.dart'; import 'package:scrollview_observer/src/gridview/models/gridview_observe_model.dart'; import 'package:scrollview_observer/src/listview/models/listview_observe_model.dart'; import 'package:scrollview_observer/src/utils/src/log.dart'; import 'dart:math' as math; class ObserverUtils { ObserverUtils._(); /// Calculate current extent of [RenderSliverPersistentHeader] base on /// target layout offset. /// Such as [SliverAppBar] /// /// You must pass either [key] or [context] static double calcPersistentHeaderExtent({ GlobalKey? key, BuildContext? context, required double offset, }) { assert(key != null || context != null); final ctx = key?.currentContext ?? context; final obj = ObserverUtils.findRenderObject(ctx); if (obj is! RenderSliverPersistentHeader) return 0; final maxExtent = obj.maxExtent; final minExtent = obj.minExtent; final currentExtent = math.max(minExtent, maxExtent - offset); return currentExtent; } /// Calculate the anchor tab index. static int calcAnchorTabIndex({ required ObserveModel observeModel, @Deprecated( 'It will be removed in version 2, please use [tabIndexes] instead', ) List? tabIndexs, List? tabIndexes, required int currentTabIndex, }) { assert( tabIndexs != null || tabIndexes != null, 'tabIndexes and tabIndexs cannot both be null.', ); List indexes = tabIndexes ?? tabIndexs ?? []; if (currentTabIndex >= indexes.length) { return currentTabIndex; } if (observeModel is ListViewObserveModel) { return calcAnchorTabIndexForList( firstIndex: observeModel.firstChild?.index, tabIndexes: indexes, currentTabIndex: currentTabIndex, ); } else if (observeModel is GridViewObserveModel) { final firstGroupChildList = observeModel.firstGroupChildList; if (firstGroupChildList.isEmpty) { return currentTabIndex; } // Record the child with the shortest distance from the bottom. GridViewObserveDisplayingChildModel mainChildModel = firstGroupChildList.first; for (var firstGroupChildModel in firstGroupChildList) { final index = indexes.indexOf(firstGroupChildModel.index); if (isValidListIndex(index)) { // Found the target index from indexes, return directly. return index; } if (mainChildModel.trailingMarginToViewport < firstGroupChildModel.trailingMarginToViewport) { mainChildModel = firstGroupChildModel; } } // Target index not found from indexes. var targetTabIndex = currentTabIndex - 1; if (targetTabIndex < 0 || targetTabIndex >= indexes.length) { return currentTabIndex; } var curIndex = indexes[currentTabIndex]; final firstGroupIndexList = firstGroupChildList.map((e) => e.index).toList(); final minOffset = mainChildModel.layoutOffset; final maxOffset = mainChildModel.layoutOffset + mainChildModel.mainAxisSize; final displayingChildModelList = observeModel.displayingChildModelList.where((e) { return !firstGroupIndexList.contains(e.index) && e.layoutOffset >= minOffset && e.layoutOffset <= maxOffset; }).toList(); // If the indexes of all the children currently being displayed are // greater than curIndex, keep using currentTabIndex. // Otherwise, using targetTabIndex. for (var model in displayingChildModelList) { if (model.index <= curIndex) { return targetTabIndex; } } } return currentTabIndex; } /// Calculate the anchor tab index for list type. /// /// - [firstIndex] is the index of the first child widget. /// - [tabIndexes] is the list of indexes of all tabs. /// - [currentTabIndex] is the current tab index. static int calcAnchorTabIndexForList({ int? firstIndex, required List tabIndexes, required int currentTabIndex, }) { // Example: // ====== exact match ====== // tabIndexes: [0, 6, 9, 11, 12, 16] // firstIndex: 12 // result: 4 (the index of 12 in tabIndexes) // // ====== no exact match ====== // tabIndexes: [0, 6, 9, 11, 12, 16] // firstIndex: 10 // result: 2 (the index of 9 in tabIndexes) if (tabIndexes.isEmpty) return currentTabIndex; if (firstIndex == null) return currentTabIndex; final target = firstIndex; // If the target value is less than the minimum value, currentTabIndex is // returned. if (target < tabIndexes.first) return currentTabIndex; // If the target value is greater than or equal to the maximum value, the // maximum value is returned. if (target >= tabIndexes.last) return tabIndexes.length - 1; // Two-point search int left = 0; int right = tabIndexes.length - 1; // The currentTabIndex is returned by default. int resultIndex = currentTabIndex; while (left <= right) { int mid = (left + right) ~/ 2; int midValue = tabIndexes[mid]; if (midValue == target) { // Find an equal value and return its index. return mid; } else if (midValue < target) { // Update the index of elements with the largest less than the target // value. resultIndex = mid; left = mid + 1; } else { right = mid - 1; } } return resultIndex; } /// Determines whether the offset at the bottom of the target child widget /// is below the specified offset. static bool isBelowOffsetWidgetInSliver({ required double scrollViewOffset, required Axis scrollDirection, required RenderBox targetChild, double toNextOverPercent = 1, }) { if (!targetChild.hasSize) return false; final parentData = targetChild.parentData; if (parentData is! SliverMultiBoxAdaptorParentData) { return false; } final targetFirstChildOffset = parentData.layoutOffset ?? 0; final double targetFirstChildSize; try { // In some cases, getting size may throw an exception. targetFirstChildSize = scrollDirection == Axis.vertical ? targetChild.size.height : targetChild.size.width; } catch (_) { return false; } return scrollViewOffset < targetFirstChildSize * toNextOverPercent + targetFirstChildOffset; } /// Determines whether the target child widget has reached the specified /// offset static bool isReachOffsetWidgetInSliver({ required double scrollViewOffset, required Axis scrollDirection, required RenderBox targetChild, double toNextOverPercent = 1, }) { if (!isBelowOffsetWidgetInSliver( scrollViewOffset: scrollViewOffset, scrollDirection: scrollDirection, targetChild: targetChild, toNextOverPercent: toNextOverPercent, )) { return false; } final parentData = targetChild.parentData; if (parentData is! SliverMultiBoxAdaptorParentData) { return false; } final targetFirstChildOffset = parentData.layoutOffset ?? 0; return scrollViewOffset >= targetFirstChildOffset; } /// Determines whether the target child widget is being displayed static bool isDisplayingChildInSliver({ required RenderBox? targetChild, required double showingChildrenMaxOffset, required double scrollViewOffset, required Axis scrollDirection, double toNextOverPercent = 1, }) { if (targetChild == null) { return false; } if (!isBelowOffsetWidgetInSliver( scrollViewOffset: scrollViewOffset, scrollDirection: scrollDirection, targetChild: targetChild, toNextOverPercent: toNextOverPercent, )) { return false; } final parentData = targetChild.parentData; if (parentData is! SliverMultiBoxAdaptorParentData) { return false; } final targetChildLayoutOffset = parentData.layoutOffset ?? 0; return targetChildLayoutOffset < showingChildrenMaxOffset; } /// Find out the viewport static RenderViewportBase? findViewport(RenderObject obj) { int maxCycleCount = 10; int currentCycleCount = 1; // Starting from flutter version 3.13.0, the type of parent received // is RenderObject, while the type of the previous version is AbstractNode, // but RenderObject is a subclass of AbstractNode, so for compatibility, // we can use RenderObject. var parent = obj.parent; if (parent is! RenderObject) { return null; } while (parent != null && currentCycleCount <= maxCycleCount) { if (parent is RenderViewportBase) { return parent; } parent = parent.parent; currentCycleCount++; } return null; } /// For viewport /// /// Determines whether the offset at the bottom of the target child widget /// is below the specified offset. static bool isBelowOffsetSliverInViewport({ required double viewportPixels, RenderSliver? sliver, }) { if (sliver == null) return false; final layoutOffset = sliver.constraints.precedingScrollExtent; final size = sliver.geometry?.maxPaintExtent ?? 0; return viewportPixels <= layoutOffset + size; } /// For viewport /// /// Determines whether the target sliver is being displayed static bool isDisplayingSliverInViewport({ required RenderSliver? sliver, required double viewportPixels, required double viewportBottomOffset, }) { if (sliver == null) { return false; } if (!(sliver.geometry?.visible ?? false)) { return false; } if (!isBelowOffsetSliverInViewport( viewportPixels: viewportPixels, sliver: sliver, )) { return false; } return sliver.constraints.precedingScrollExtent < viewportBottomOffset; } /// Determines whether it is a valid list index. static bool isValidListIndex(int index) { return index != -1; } /// Safely call findRenderObject method. static RenderObject? findRenderObject(BuildContext? context) { bool isMounted = context?.mounted ?? false; if (!isMounted) { return null; } try { // It throws an exception when getting renderObject of inactive element. return context?.findRenderObject(); } catch (e) { Log.warning('Cannot get renderObject of inactive element.\n' 'Please call the reattach method of ObserverController to re-record ' 'BuildContext.'); return null; } } /// Walks the children of this context. static BuildContext? findChildContext({ required BuildContext context, required bool Function(Element) isTargetType, }) { BuildContext? childContext; void visitor(Element element) { if (isTargetType(element)) { /// Find the target child BuildContext childContext = element; return; } element.visitChildren(visitor); } try { // https://github.com/fluttercandies/flutter_scrollview_observer/issues/35 context.visitChildElements(visitor); } catch (e) { Log.warning( 'This widget has been unmounted, so the State no longer has a context (and should be considered defunct). \n' 'Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.', ); } return childContext; } /// Convert the given point from the local coordinate system for this context /// to the global coordinate system in logical pixels. /// /// If `ancestor` is non-null, this function converts the given point to the /// coordinate system of `ancestor` (which must be an ancestor of this render /// object of context) instead of to the global coordinate system. /// /// This method is implemented in terms of [getTransformTo]. If the transform /// matrix puts the given `point` on the line at infinity (for instance, when /// the transform matrix is the zero matrix), this method returns (NaN, NaN). static Offset? localToGlobal({ required BuildContext context, required Offset point, BuildContext? ancestor, }) { final renderObject = findRenderObject(context); if (renderObject == null) return null; return MatrixUtils.transformPoint( renderObject.getTransformTo(findRenderObject(ancestor)), point, ); } /// Safely obtain [RenderSliver.constraints]. static SliverConstraints? sliverConstraints( RenderSliver sliver, ) { SliverConstraints? _constraints; try { _constraints = sliver.constraints; } catch (e) { Log.warning( 'A RenderObject does not have any constraints before it has been laid out.', ); } return _constraints; } } ================================================ FILE: lib/src/utils/src/slivers.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class SliverObserveContext extends SliverPadding { final void Function(BuildContext) onObserve; const SliverObserveContext({ Key? key, Widget? child, required this.onObserve, }) : super( key: key, padding: EdgeInsets.zero, sliver: child, ); @override RenderSliverPadding createRenderObject(BuildContext context) { onObserve.call(context); return super.createRenderObject(context); } } class SliverObserveContextToBoxAdapter extends SliverToBoxAdapter { final void Function(BuildContext) onObserve; const SliverObserveContextToBoxAdapter({ Key? key, required Widget? child, required this.onObserve, }) : super(key: key, child: child); @override RenderSliverToBoxAdapter createRenderObject(BuildContext context) { onObserve.call(context); return super.createRenderObject(context); } } ================================================ FILE: listview_observer.iml ================================================ ================================================ FILE: pubspec.yaml ================================================ name: scrollview_observer description: A widget for observing data related to the child widgets being displayed in a ScrollView. version: 1.26.3 homepage: https://github.com/fluttercandies/flutter_scrollview_observer environment: sdk: ">=2.19.0 <4.0.0" flutter: '>=3.7.0' dependencies: flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^1.0.0 ================================================ FILE: test/chat_scroll_observer_test.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2023-11-25 19:04:30 */ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; void main() { // Regression test for https://github.com/fluttercandies/flutter_scrollview_observer/issues/64. testWidgets('Keeping position', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController(controller: scrollController); final chatScrollObserver = ChatScrollObserver(observerController) ..fixedPositionOffset = -1; int receiveScrollNotificationCount = 0; Widget widget = ChatListView( scrollController: scrollController, observerController: observerController, chatScrollObserver: chatScrollObserver, onReceiveScrollNotification: () { receiveScrollNotificationCount += 1; }, ); await tester.pumpWidget(widget); await tester.tap(find.byIcon(Icons.add)); await tester.pumpAndSettle(); final result = await observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); expect(result.observeResult?.firstChild?.index, 4); expect(receiveScrollNotificationCount, 0); scrollController.dispose(); }); testWidgets('Keeping position with ChatScrollObserverHandleMode.specified', (tester) async { GlobalKey key = GlobalKey(); final scrollController = ScrollController(); final observerController = ListObserverController(controller: scrollController); final chatScrollObserver = ChatScrollObserver(observerController) ..fixedPositionOffset = -1; const firstDisplayingChildIndex = 2; Widget widget = ChatListView( key: key, scrollController: scrollController, observerController: observerController, chatScrollObserver: chatScrollObserver, ); await tester.pumpWidget(widget); updateData({ int index = 0, }) { key.currentState?.updateData( index: index, needSetState: true, ); } observerController.jumpTo(index: firstDisplayingChildIndex); await tester.pumpAndSettle(); var result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expectSync( result.observeResult?.firstChild?.index, firstDisplayingChildIndex, ); expectSync(result.observeResult?.firstChild?.leadingMarginToViewport, 0); // relativeIndexStartFromCacheExtent var firstItemModel = observerController.observeFirstItem(); var firstItemIndex = firstItemModel?.index ?? 0; await chatScrollObserver.standby( mode: ChatScrollObserverHandleMode.specified, refIndexType: ChatScrollObserverRefIndexType.relativeIndexStartFromCacheExtent, refItemIndex: 1, refItemIndexAfterUpdate: 1, ); expect(chatScrollObserver.refItemIndex, firstItemIndex + 1); expect( chatScrollObserver.refItemIndexAfterUpdate, firstItemIndex + 1, ); updateData(); await tester.pumpAndSettle(); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expectSync( result.observeResult?.firstChild?.index, firstDisplayingChildIndex, ); expectSync(result.observeResult?.firstChild?.leadingMarginToViewport, 0); // relativeIndexStartFromDisplaying result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); var currentFirstDisplayingChildIndex = result.observeResult?.firstChild?.index ?? 0; await chatScrollObserver.standby( mode: ChatScrollObserverHandleMode.specified, refIndexType: ChatScrollObserverRefIndexType.relativeIndexStartFromDisplaying, refItemIndex: 1, refItemIndexAfterUpdate: 1, ); expect( chatScrollObserver.refItemIndex, currentFirstDisplayingChildIndex + 1, ); expect( chatScrollObserver.refItemIndexAfterUpdate, currentFirstDisplayingChildIndex + 1, ); updateData(); await tester.pumpAndSettle(); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expectSync( result.observeResult?.firstChild?.index, firstDisplayingChildIndex, ); expectSync(result.observeResult?.firstChild?.leadingMarginToViewport, 0); // itemIndex await chatScrollObserver.standby( mode: ChatScrollObserverHandleMode.specified, refIndexType: ChatScrollObserverRefIndexType.itemIndex, refItemIndex: firstDisplayingChildIndex, refItemIndexAfterUpdate: firstDisplayingChildIndex, ); updateData(); await tester.pumpAndSettle(); expect(chatScrollObserver.refItemIndex, firstDisplayingChildIndex); expect( chatScrollObserver.refItemIndexAfterUpdate, firstDisplayingChildIndex, ); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expectSync( result.observeResult?.firstChild?.index, firstDisplayingChildIndex, ); expectSync(result.observeResult?.firstChild?.leadingMarginToViewport, 0); scrollController.dispose(); }); testWidgets( 'Keeping position with ChatScrollObserverHandleMode.specified when item changes by itself', (tester) async { GlobalKey key = GlobalKey(); final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); final chatScrollObserver = ChatScrollObserver(observerController) ..fixedPositionOffset = -1; const observeItemSelfIndex = 0; Widget widget = ChatListView( key: key, scrollController: scrollController, observerController: observerController, chatScrollObserver: chatScrollObserver, itemBuilder: (context, index) { final dataList = key.currentState?.dataList ?? []; return Text( dataList[index], maxLines: 999, ); }, dataList: ['initData' * 500], ); await tester.pumpWidget(widget); void updateData({ int index = 0, }) { key.currentState?.updateData( index: index, needSetState: true, ); } await tester.pumpAndSettle(); var result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expectSync( result.observeResult?.firstChild?.index ?? 0, observeItemSelfIndex, ); expectSync( result.observeResult?.firstChild?.mainAxisSize, greaterThanOrEqualTo( result.observeResult?.viewport.paintBounds.height ?? 0, ), ); final previousTrailingMarginToViewport = result.observeResult?.firstChild?.trailingMarginToViewport; // itemIndex await chatScrollObserver.standby( mode: ChatScrollObserverHandleMode.specified, refIndexType: ChatScrollObserverRefIndexType.itemIndex, refItemIndex: observeItemSelfIndex, refItemIndexAfterUpdate: observeItemSelfIndex, customAdjustPositionDelta: (model) { return model.newPosition.extentAfter - model.oldPosition.extentAfter; }, ); updateData(); await tester.pumpAndSettle(); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expect(chatScrollObserver.refItemIndex, observeItemSelfIndex); expect( chatScrollObserver.refItemIndexAfterUpdate, observeItemSelfIndex, ); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expectSync( result.observeResult?.firstChild?.index, observeItemSelfIndex, ); expectSync( result.observeResult?.firstChild?.trailingMarginToViewport, previousTrailingMarginToViewport, ); scrollController.dispose(); }); testWidgets('Keeping position with customAdjustPositionDelta', (tester) async { GlobalKey key = GlobalKey(); final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); final chatScrollObserver = ChatScrollObserver(observerController) ..fixedPositionOffset = -1; Map itemHeightMap = {}; const double expandedItemHeight = 200; const double normalItemHeight = 100; Widget widget = ChatListView( key: key, scrollController: scrollController, observerController: observerController, chatScrollObserver: chatScrollObserver, itemBuilder: (context, index) { if (itemHeightMap[index] == null) { itemHeightMap[index] = normalItemHeight; } double itemHeight = itemHeightMap[index] ?? normalItemHeight; return SizedBox( height: itemHeight, child: Center(child: Text(index.toString())), ); }, ); await tester.pumpWidget(widget); Future setState() async { await tester.tap(find.byIcon(Icons.refresh)); await tester.pumpAndSettle(); } var result = await observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); var observeResult = result.observeResult; final displayingChildModelList = observeResult?.displayingChildModelList ?? []; expect(displayingChildModelList, isNotEmpty); expect(observeResult?.firstChild?.index, 0); expect(observeResult?.firstChild?.leadingMarginToViewport, 0); // Check adjustPosition. key.currentState?.updateData( index: 1, needSetState: true, ); double? adjustPosition; await chatScrollObserver.standby( mode: ChatScrollObserverHandleMode.specified, refIndexType: ChatScrollObserverRefIndexType.itemIndex, refItemIndex: 0, refItemIndexAfterUpdate: 0, customAdjustPositionDelta: (model) { adjustPosition = model.adjustPosition; return null; }, ); await tester.pumpAndSettle(); expect(adjustPosition, 0); result = await observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); observeResult = result.observeResult; expect(observeResult?.firstChild?.index, 0); expect(observeResult?.firstChild?.leadingMarginToViewport, 0); final targetIndex = displayingChildModelList.last.index + 1; // Jump to targetIndex and align its bottom with the viewport bottom. observerController.jumpTo( index: targetIndex, offset: (targetOffset) { final viewportMainAxisExtent = observeResult?.firstChild?.viewportMainAxisExtent ?? 0; return viewportMainAxisExtent - normalItemHeight; }, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); // Check if the last item is aligned with the viewport bottom. result = await observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); observeResult = result.observeResult; var lastDisplayingChildModel = observeResult?.displayingChildModelList.last; expect(lastDisplayingChildModel?.index, targetIndex); expect(lastDisplayingChildModel?.trailingMarginToViewport, 0); // Expand the last item. itemHeightMap[targetIndex] = expandedItemHeight; final refItemIndex = targetIndex; await chatScrollObserver.standby( mode: ChatScrollObserverHandleMode.specified, refIndexType: ChatScrollObserverRefIndexType.itemIndex, refItemIndex: refItemIndex, refItemIndexAfterUpdate: refItemIndex, customAdjustPositionDelta: (model) { return expandedItemHeight - normalItemHeight; }, ); await setState(); result = await observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); observeResult = result.observeResult; lastDisplayingChildModel = observeResult?.displayingChildModelList.last; expect(lastDisplayingChildModel?.index, targetIndex); expect(lastDisplayingChildModel?.trailingMarginToViewport, 0); // Restore the last item to normal height. itemHeightMap[targetIndex] = normalItemHeight; await chatScrollObserver.standby( mode: ChatScrollObserverHandleMode.specified, refIndexType: ChatScrollObserverRefIndexType.itemIndex, refItemIndex: refItemIndex, refItemIndexAfterUpdate: refItemIndex, customAdjustPositionDelta: (model) { adjustPosition = model.adjustPosition; return normalItemHeight - expandedItemHeight; }, ); await setState(); result = await observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); observeResult = result.observeResult; lastDisplayingChildModel = observeResult?.displayingChildModelList.last; expect(lastDisplayingChildModel?.index, targetIndex); expect(lastDisplayingChildModel?.trailingMarginToViewport, 0); expect(adjustPosition, greaterThan(0)); scrollController.dispose(); }); testWidgets('Keeping position with customAdjustPosition', (tester) async { GlobalKey key = GlobalKey(); final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); ChatScrollObserverHandlePositionResultModel? onHandlePositionResultModel; final chatScrollObserver = ChatScrollObserver(observerController) ..onHandlePositionResultCallback = (model) { onHandlePositionResultModel = model; }; Widget widget = ChatListView( key: key, scrollController: scrollController, observerController: observerController, chatScrollObserver: chatScrollObserver, itemBuilder: (context, index) { final dataList = key.currentState?.dataList ?? []; return Text(dataList[index]); }, ); await tester.pumpWidget(widget); final chatListViewState = key.currentState; expect(chatListViewState, isNotNull); final dataListLength = chatListViewState?.dataList.length ?? 0; final lastIndex = dataListLength - 1; observerController.jumpTo(index: lastIndex); await tester.pumpAndSettle(); var result = await observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); var lastModel = result.observeResult?.displayingChildModelList.last; expect(lastModel?.index, lastIndex); expect(lastModel?.trailingMarginToViewport, 0); expect(onHandlePositionResultModel, isNull); chatListViewState?.updateData( index: lastIndex, needSetState: true, ); await chatScrollObserver.standby( customAdjustPosition: (model) { final delta = model.newPosition.extentAfter - model.oldPosition.extentAfter; return model.adjustPosition + delta; }, ); await tester.pumpAndSettle(); result = await observerController.dispatchOnceObserve( isForce: true, isDependObserveCallback: false, ); lastModel = result.observeResult?.displayingChildModelList.last; expect(lastModel?.index, lastIndex); expect(lastModel?.trailingMarginToViewport, 0); expect( onHandlePositionResultModel?.type, ChatScrollObserverHandlePositionType.keepPosition, ); scrollController.dispose(); }); } class ChatListView extends StatefulWidget { const ChatListView({ Key? key, required this.scrollController, required this.observerController, required this.chatScrollObserver, this.onReceiveScrollNotification, this.itemBuilder, this.dataList, }) : super(key: key); final ScrollController scrollController; final ListObserverController observerController; final ChatScrollObserver chatScrollObserver; final Function()? onReceiveScrollNotification; final NullableIndexedWidgetBuilder? itemBuilder; final List? dataList; @override State createState() => ChatListViewState(); } class ChatListViewState extends State { late List dataList = widget.dataList ?? List.generate(100, (index) => index.toString()).toList(); void initData({ required List dataList, bool needSetState = true, }) { this.dataList = dataList; if (needSetState) { setState(() {}); } } void updateData({ int index = 0, String? appendStr, bool needSetState = true, }) { dataList[index] += appendStr ?? 'updateData' * 10; if (needSetState) { setState(() {}); } } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(), body: _buildListView(), floatingActionButton: Column( verticalDirection: VerticalDirection.up, children: [ FloatingActionButton( onPressed: () { widget.chatScrollObserver.standby(changeCount: 4); setState(() { dataList.insert(0, '-1'); dataList.insert(0, '-2'); dataList.insert(0, '-3'); dataList.insert(0, '-4'); }); }, child: const Icon(Icons.add), ), FloatingActionButton( onPressed: () { setState(() {}); }, child: const Icon(Icons.refresh), ), ], ), ), ); } Widget _buildListView() { Widget resultWidget = ListViewObserver( controller: widget.observerController, child: ListView.builder( itemCount: dataList.length, physics: ChatObserverBouncingScrollPhysics( observer: widget.chatScrollObserver, ), controller: widget.scrollController, itemBuilder: widget.itemBuilder ?? (context, index) { return SizedBox( height: 100, child: Center(child: Text(dataList[index])), ); }, ), ); resultWidget = NotificationListener( child: resultWidget, onNotification: (notification) { widget.onReceiveScrollNotification?.call(); return false; }, ); return resultWidget; } } ================================================ FILE: test/grid_observer_test.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/observer_widget_tag_manager.dart'; void main() { Widget getGridView({ ScrollController? scrollController, int itemCount = 200, NullableIndexedWidgetBuilder? itemBuilder, }) { return Directionality( textDirection: TextDirection.ltr, child: GridView.builder( controller: scrollController, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 2, mainAxisSpacing: 5, ), itemBuilder: (context, index) { if (itemBuilder != null) { return itemBuilder(context, index); } return Container( color: Colors.blue[100], child: Center( child: Text('index -- $index'), ), ); }, itemCount: itemCount, ), ); } testWidgets('Auto get target sliver context', (tester) async { final scrollController = ScrollController(); final gridObserverController = GridObserverController( controller: scrollController, ); Widget widget = getGridView( scrollController: scrollController, ); widget = GridViewObserver( child: widget, controller: gridObserverController, ); await tester.pumpWidget(widget); expect(gridObserverController.sliverContexts.length, 1); scrollController.dispose(); }); testWidgets('scrollNotificationPredicate', (tester) async { final scrollController = ScrollController(); final observerController = GridObserverController( controller: scrollController, ); final pageController = PageController(); bool isCalledOnObserve = false; Widget widget = getGridView( scrollController: scrollController, itemBuilder: (ctx, index) { if (index == 0) { return PageView.builder( scrollDirection: Axis.horizontal, controller: pageController, itemBuilder: (ctx, index) { return const SizedBox.expand(); }, itemCount: 10, ); } return const SizedBox.expand(); }, ); widget = GridViewObserver( child: widget, controller: observerController, scrollNotificationPredicate: defaultScrollNotificationPredicate, onObserve: (result) { isCalledOnObserve = true; }, ); await tester.pumpWidget(widget); pageController.animateToPage( 3, duration: const Duration(milliseconds: 100), curve: Curves.easeInOut, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(pageController.page, 3); expect(isCalledOnObserve, isFalse); scrollController.animateTo( 10, duration: const Duration(milliseconds: 100), curve: Curves.easeInOut, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserve, isTrue); scrollController.dispose(); pageController.dispose(); }); testWidgets('Scroll to index', (tester) async { final scrollController = ScrollController(); final gridObserverController = GridObserverController( controller: scrollController, ); Widget widget = getGridView( scrollController: scrollController, ); GridViewObserveModel? observeResult; widget = GridViewObserver( child: widget, controller: gridObserverController, onObserve: (result) { observeResult = result; }, ); await tester.pumpWidget(widget); int targetItemIndex = 30; gridObserverController.jumpTo(index: targetItemIndex); await tester.pumpAndSettle(); expect( (observeResult?.firstGroupChildList.map((e) => e.index) ?? []) .contains(targetItemIndex), true, ); targetItemIndex = 60; gridObserverController.animateTo( index: targetItemIndex, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); await tester.pumpAndSettle(); expect( (observeResult?.firstGroupChildList.map((e) => e.index) ?? []) .contains(targetItemIndex), true, ); scrollController.dispose(); }); group('Cache index offset', () { testWidgets('Property cacheJumpIndexOffset', (tester) async { final scrollController = ScrollController(); final observerController = GridObserverController( controller: scrollController, )..cacheJumpIndexOffset = false; Widget widget = getGridView( scrollController: scrollController, ); widget = GridViewObserver( child: widget, controller: observerController, ); await tester.pumpWidget(widget); observerController.jumpTo(index: 30); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observerController.indexOffsetMap.isEmpty, true); scrollController.dispose(); }); testWidgets('Method clearScrollIndexCache', (tester) async { final scrollController = ScrollController(); final observerController = GridObserverController( controller: scrollController, ); Widget widget = getGridView( scrollController: scrollController, ); widget = GridViewObserver( child: widget, controller: observerController, ); await tester.pumpWidget(widget); final ctx = observerController.fetchSliverContext(); expect(ctx != null, true); observerController.jumpTo(index: 30); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observerController.indexOffsetMap[ctx]?.isNotEmpty, true); observerController.clearScrollIndexCache(); expect(observerController.indexOffsetMap[ctx]?.isEmpty, true); scrollController.dispose(); }); }); testWidgets('Check displayPercentage', (tester) async { final scrollController = ScrollController(); final observerController = GridObserverController( controller: scrollController, ); Widget widget = getGridView( scrollController: scrollController, ); GridViewObserveModel? observeResult; widget = GridViewObserver( child: widget, controller: observerController, onObserve: (result) { observeResult = result; }, ); await tester.pumpWidget(widget); int targetItemIndex = 30; observerController.jumpTo( index: targetItemIndex, alignment: 0, ); await tester.pumpAndSettle(); var firstGroupChildList = observeResult?.firstGroupChildList ?? []; expect(firstGroupChildList, isNotEmpty); expect(firstGroupChildList.first.index, targetItemIndex); expect(firstGroupChildList.first.displayPercentage, 1); observerController.jumpTo( index: targetItemIndex, alignment: 0.5, ); await tester.pumpAndSettle(); firstGroupChildList = observeResult?.firstGroupChildList ?? []; expect(firstGroupChildList.first.index, targetItemIndex); expect(firstGroupChildList.first.displayPercentage, 0.5); observerController.jumpTo( index: targetItemIndex, alignment: 1, ); await tester.pumpAndSettle(); firstGroupChildList = observeResult?.firstGroupChildList ?? []; expect(firstGroupChildList.first.index, targetItemIndex + 2); scrollController.dispose(); }); testWidgets('Check observeIntervalForScrolling', (tester) async { final scrollController = ScrollController(); final observerController = GridObserverController( controller: scrollController, )..observeIntervalForScrolling = const Duration(milliseconds: 500); int observeCount = 0; Widget widget = getGridView( scrollController: scrollController, ); widget = GridViewObserver( child: widget, controller: observerController, autoTriggerObserveTypes: const [ ObserverAutoTriggerObserveType.scrollStart, ObserverAutoTriggerObserveType.scrollUpdate, ObserverAutoTriggerObserveType.scrollEnd, ], triggerOnObserveType: ObserverTriggerOnObserveType.directly, onObserve: (result) { observeCount++; }, ); await tester.pumpWidget(widget); final finder = find.byWidget(widget); await tester.timedDragFrom( tester.getCenter(finder), const Offset(0, -50), const Duration(milliseconds: 400), ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observeCount, 3); observeCount = 0; await tester.timedDragFrom( tester.getCenter(finder), const Offset(0, -50), const Duration(milliseconds: 600), ); await tester.pumpAndSettle(); await tester.pump(const Duration(milliseconds: 600)); expect(observeCount, 4); scrollController.dispose(); }); testWidgets('Check isForbidObserveCallback', (tester) async { final scrollController = ScrollController(); final observerController = GridObserverController(controller: scrollController); Widget widget = getGridView( scrollController: scrollController, ); bool isCalledOnObserve = false; widget = GridViewObserver( child: widget, controller: observerController, onObserve: (result) { isCalledOnObserve = true; }, ); await tester.pumpWidget(widget); observerController.isForbidObserveCallback = true; observerController.jumpTo(index: 10); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserve, false); observerController.isForbidObserveCallback = false; observerController.jumpTo(index: 30); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserve, true); scrollController.dispose(); }); testWidgets('Check displayingChildModelMap', (tester) async { final scrollController = ScrollController(); final observerController = GridObserverController( controller: scrollController, ); Widget widget = getGridView( scrollController: scrollController, ); GridViewObserveModel? observeResult; widget = GridViewObserver( child: widget, controller: observerController, onObserve: (result) { observeResult = result; }, ); await tester.pumpWidget(widget); await observerController.dispatchOnceObserve( isForce: true, ); expect(observeResult, isNotNull); var firstGroupChildList = observeResult?.firstGroupChildList ?? []; expect(firstGroupChildList, isNotEmpty); expect(firstGroupChildList.first.index, 0); expect( listEquals( observeResult?.displayingChildModelMap.values.toList(), observeResult?.displayingChildModelList, ), true, ); int targetItemIndex = 30; observerController.jumpTo( index: targetItemIndex, alignment: 0, ); await tester.pumpAndSettle(); firstGroupChildList = observeResult?.firstGroupChildList ?? []; expect(firstGroupChildList, isNotEmpty); expect(firstGroupChildList.first.index, targetItemIndex); expect( listEquals( observeResult?.displayingChildModelMap.values.toList(), observeResult?.displayingChildModelList, ), true, ); scrollController.dispose(); }); group( 'ObserverScrollNotification', () { late ScrollController scrollController; late GridObserverController observerController; late Widget widget; int indexOfStartNoti = -1; int indexOfInterruptionNoti = -1; int indexOfDecisionNoti = -1; int indexOfEndNoti = -1; resetAll({ bool isFixedHeight = false, }) { indexOfStartNoti = -1; indexOfInterruptionNoti = -1; indexOfDecisionNoti = -1; indexOfEndNoti = -1; scrollController = ScrollController(); observerController = GridObserverController(controller: scrollController); widget = getGridView( scrollController: scrollController, itemCount: 100, ); widget = GridViewObserver( child: widget, controller: observerController, ); int count = 0; widget = NotificationListener( child: widget, onNotification: (notification) { if (notification is ObserverScrollStartNotification) { indexOfStartNoti = count; } else if (notification is ObserverScrollInterruptionNotification) { indexOfInterruptionNoti = count; } else if (notification is ObserverScrollDecisionNotification) { indexOfDecisionNoti = count; } else if (notification is ObserverScrollEndNotification) { indexOfEndNoti = count; } count += 1; return true; }, ); } tearDown(() { scrollController.dispose(); }); testWidgets( 'Notification sequence in normal scenarios', (tester) async { resetAll(); await tester.pumpWidget(widget); observerController.jumpTo(index: 10); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(indexOfStartNoti, 0); expect(indexOfInterruptionNoti, -1); expect(indexOfDecisionNoti, 1); expect(indexOfEndNoti, 2); }, ); testWidgets( 'Notification sequence when using incorrect index', (tester) async { resetAll(); await tester.pumpWidget(widget); observerController.jumpTo(index: 101); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(indexOfStartNoti, 0); expect(indexOfInterruptionNoti, 1); expect(indexOfDecisionNoti, -1); expect(indexOfEndNoti, -1); }, ); }, ); group('dispatchOnceObserve', () { late ScrollController scrollController; late GridObserverController observerController; late Widget widget; tearDown(() { scrollController.dispose(); }); resetAll() { scrollController = ScrollController(); observerController = GridObserverController( controller: scrollController, ); widget = getGridView( scrollController: scrollController, itemCount: 100, ); widget = GridViewObserver( child: widget, controller: observerController, ); } testWidgets( 'Check observeResult', (tester) async { resetAll(); await tester.pumpWidget(widget); var result = await observerController.dispatchOnceObserve(); expect(result.isSuccess, isFalse); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect(result.observeResult, isNotNull); expect( result.observeResult?.displayingChildIndexList ?? [], isNotEmpty, ); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect( result.observeResult?.displayingChildIndexList ?? [], isEmpty, ); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expect(result.isSuccess, isTrue); expect( result.observeResult?.displayingChildIndexList ?? [], isNotEmpty, ); }, ); testWidgets( 'Check observeAllResult', (tester) async { resetAll(); await tester.pumpWidget(widget); var result = await observerController.dispatchOnceObserve(); expect(result.isSuccess, isFalse); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect(result.observeAllResult.values.length, 1); expect( result.observeAllResult.values.first, result.observeResult, ); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect(result.observeAllResult, isEmpty); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expect(result.isSuccess, isTrue); expect(result.observeAllResult, isNotEmpty); }, ); }); group('ObserverListener', () { late String tag1; late String tag2; late GlobalKey key1; late GlobalKey key2; late ScrollController scrollController; late GridObserverController observerController1; late GridObserverController observerController2; late Widget widget; tearDown(() { scrollController.dispose(); }); resetAll({ bool isResetTag = false, }) { if (isResetTag) { tag1 = tag1 * 2; tag2 = tag2 * 2; } else { tag1 = 'tag1'; tag2 = 'tag2'; key1 = GlobalKey(); key2 = GlobalKey(); scrollController = ScrollController(); observerController1 = GridObserverController( controller: scrollController, ); observerController2 = GridObserverController( controller: scrollController, ); } widget = getGridView( scrollController: scrollController, itemCount: 100, itemBuilder: (context, index) { if (index == 3) { return const Center( child: Icon(Icons.abc), ); } return Container( color: Colors.blue, child: Center( child: Text('index -- $index'), ), ); }, ); widget = GridViewObserver( key: key2, tag: tag2, child: widget, controller: observerController2, ); widget = GridViewObserver( key: key1, tag: tag1, child: widget, controller: observerController1, ); } testWidgets('Change tag', (tester) async { resetAll(); await tester.pumpWidget(widget); final itemFinder = find.byIcon(Icons.abc); ObserverWidgetTagManager? tagManager = ObserverWidgetTagManager.maybeOf( tester.element(itemFinder), ); expect(tagManager, isNotNull); Map tagMap = Map.from(tagManager?.tagMap ?? {}); Set tags = {tag1, tag2}; expect(tagMap.keys.toSet(), tags); resetAll(isResetTag: true); await tester.pumpWidget(widget); tagManager = ObserverWidgetTagManager.maybeOf( tester.element(itemFinder), ); tagMap = Map.from(tagManager?.tagMap ?? {}); Set newTags = {tag1, tag2}; expect(tags != newTags, isTrue); expect(tagMap.keys.toSet(), newTags); }); testWidgets('of', (tester) async { resetAll(); await tester.pumpWidget(widget); final itemFinder = find.byIcon(Icons.abc); GridViewObserveModel? cbResult; Map? cbAllResult; onObserveCallback(GridViewObserveModel result) { cbResult = result; } onObserveAllCallback( Map resultMap, ) { cbAllResult = resultMap; } final observerState = GridViewObserver.of( tester.element(itemFinder), ); expect(observerState, isNotNull); final observerStateByTag2 = GridViewObserver.of( tester.element(itemFinder), tag: tag2, ); expect(observerStateByTag2 == key2.currentState, isTrue); expect(observerStateByTag2 == observerState, isTrue); final observerStateByTag1 = GridViewObserver.of( tester.element(itemFinder), tag: tag1, ); expect(observerStateByTag1 == key1.currentState, isTrue); expect(observerStateByTag1 == observerState, isFalse); observerState.addListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, ); expect(observerState.innerListeners?.length, 1); final result = await observerController2.dispatchOnceObserve(); expect(result.observeResult, cbResult); expect(result.observeAllResult, cbAllResult); observerState.removeListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, ); expect(observerState.innerListeners?.length, 0); }); testWidgets('maybeOf', (tester) async { resetAll(); await tester.pumpWidget(widget); final itemFinder = find.byIcon(Icons.abc); GridViewObserverState? observerState = GridViewObserver.maybeOf( tester.element(find.byKey(key1)), ); expect(observerState, isNull); GridViewObserveModel? cbResult; Map? cbAllResult; onObserveCallback(GridViewObserveModel result) { cbResult = result; } onObserveAllCallback( Map resultMap, ) { cbAllResult = resultMap; } observerState = GridViewObserver.maybeOf( tester.element(itemFinder), ); expect(observerState, isNotNull); final observerStateByTag2 = GridViewObserver.maybeOf( tester.element(itemFinder), tag: tag2, ); expect(observerStateByTag2 == key2.currentState, isTrue); expect(observerStateByTag2 == observerState, isTrue); final observerStateByTag1 = GridViewObserver.maybeOf( tester.element(itemFinder), tag: tag1, ); expect(observerStateByTag1 == key1.currentState, isTrue); expect(observerStateByTag1 == observerState, isFalse); observerState?.addListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, ); expect(observerState?.innerListeners?.length, 1); final result = await observerController2.dispatchOnceObserve(); expect(result.observeResult, cbResult); expect(result.observeAllResult, cbAllResult); observerState?.removeListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, ); expect(observerState?.innerListeners?.length, 0); }); testWidgets( 'innerTagChangeCount should not increase when tag remains unchanged', (tester) async { const String tag = 'tag1'; scrollController = ScrollController(); Widget gridView = getGridView(scrollController: scrollController); widget = GridViewObserver( tag: tag, child: gridView, ); await tester.pumpWidget(widget); await tester.pumpAndSettle(); // Get ObserverWidgetState final itemFinder = find.byType(GridViewObserver); final observerState = tester.state(itemFinder); expect(observerState, isNotNull); // Record initial tagChangeCount final initialTagChangeCount = observerState.innerTagChangeCount; // Refresh widget but keep tag unchanged widget = GridViewObserver( tag: tag, child: gridView, ); await tester.pumpWidget(widget); await tester.pumpAndSettle(); // Verify that tagChangeCount has not increased expect(observerState.innerTagChangeCount, initialTagChangeCount); expect(observerState.innerTagChangeCount, 0); }); testWidgets('No exception in _checkTagChange during refresh and dispose', (tester) async { // Regression test for https://github.com/fluttercandies/flutter_scrollview_observer/issues/143 scrollController = ScrollController(); Widget gridView = getGridView(scrollController: scrollController); widget = GridViewObserver( tag: 'tag1', child: gridView, ); await tester.pumpWidget(widget); // Get ObserverWidgetState final itemFinder = find.byType(GridViewObserver); final observerState = tester.state(itemFinder); // Create a controllable Future final completer = Completer(); observerState.innerCheckTagChangeEndOfFrame = completer.future; widget = GridViewObserver( tag: 'tag2', child: gridView, ); await tester.pumpWidget(widget); // Dispose widget before endOfFrame completes await tester.pumpWidget(Container()); // Complete Future to simulate endOfFrame callback execution // (widget has already been disposed at this point) completer.complete(); // Verify no exception is thrown await tester.pumpAndSettle(); }); }); } ================================================ FILE: test/list_observer_test.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/observer_widget_tag_manager.dart'; void main() { Widget getListView({ ScrollPhysics? physics, ScrollController? scrollController, int itemCount = 100, bool isFixedHeight = false, double? cacheExtent, NullableIndexedWidgetBuilder? itemBuilder, IndexedWidgetBuilder? separatorBuilder, }) { return Directionality( textDirection: TextDirection.ltr, child: ListView.separated( physics: physics, controller: scrollController, itemBuilder: (ctx, index) { if (itemBuilder != null) { return itemBuilder(ctx, index); } double height = 80; if (!isFixedHeight) { height = (index % 2 == 0) ? 80 : 50; } return SizedBox( height: height, child: Center( child: Text("index -- $index"), ), ); }, separatorBuilder: separatorBuilder ?? (ctx, index) { return const SizedBox(height: 10); }, itemCount: itemCount, cacheExtent: cacheExtent, ), ); } Widget getFixedHeightListView({ ScrollController? scrollController, int itemCount = 100, double? cacheExtent, bool useItemExtentBuilder = false, }) { const double height = 80; return Directionality( textDirection: TextDirection.ltr, child: ListView.builder( controller: scrollController, itemBuilder: (ctx, index) { return SizedBox( height: height, child: Center( child: Text("index -- $index"), ), ); }, itemExtent: useItemExtentBuilder ? null : height, itemExtentBuilder: useItemExtentBuilder ? (index, dimensions) { return height; } : null, itemCount: itemCount, cacheExtent: cacheExtent, ), ); } testWidgets('Auto get target sliver context', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); Widget widget = getListView( scrollController: scrollController, ); widget = ListViewObserver( child: widget, controller: observerController, ); await tester.pumpWidget(widget); expect(observerController.sliverContexts.length, 1); scrollController.dispose(); }); testWidgets('scrollNotificationPredicate', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); final pageController = PageController(); bool isCalledOnObserve = false; Widget widget = getListView( scrollController: scrollController, itemBuilder: (ctx, index) { if (index == 0) { return SizedBox( height: 200, child: PageView.builder( scrollDirection: Axis.horizontal, controller: pageController, itemBuilder: (ctx, index) { return const SizedBox.expand(); }, itemCount: 10, ), ); } return const SizedBox(height: 80); }, ); widget = ListViewObserver( child: widget, controller: observerController, scrollNotificationPredicate: defaultScrollNotificationPredicate, onObserve: (result) { isCalledOnObserve = true; }, ); await tester.pumpWidget(widget); pageController.animateToPage( 3, duration: const Duration(milliseconds: 100), curve: Curves.easeInOut, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(pageController.page, 3); expect(isCalledOnObserve, isFalse); scrollController.animateTo( 10, duration: const Duration(milliseconds: 100), curve: Curves.easeInOut, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserve, isTrue); scrollController.dispose(); pageController.dispose(); }); testWidgets('Check observation result after fast scrolling', (tester) async { // Regression test for https://github.com/fluttercandies/flutter_scrollview_observer/issues/113 final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); const int itemCount = 50; Widget widget = getListView( scrollController: scrollController, physics: const ClampingScrollPhysics(), itemCount: itemCount, itemBuilder: (context, index) => Text('$index'), separatorBuilder: (context, index) => const SizedBox.shrink(), ); int? lastItemIndex; widget = ListViewObserver( child: widget, controller: observerController, triggerOnObserveType: ObserverTriggerOnObserveType.directly, onObserve: (result) { lastItemIndex = result.displayingChildModelList.last.index; }, ); await tester.pumpWidget(widget); var result = await observerController.dispatchOnceObserve(); final offset = result.observeResult?.viewport.offset as ScrollPositionWithSingleContext; final maxScrollExtent = offset.maxScrollExtent; scrollController.animateTo( maxScrollExtent * 1000, duration: const Duration(milliseconds: 100), curve: Curves.easeOut, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(lastItemIndex, itemCount - 1); scrollController.dispose(); }); group('Scroll to index', () { testWidgets('Dynamic Height', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); Widget widget = getListView( scrollController: scrollController, ); ListViewObserveModel? observeResult; widget = ListViewObserver( child: widget, controller: observerController, onObserve: (result) { observeResult = result; }, ); await tester.pumpWidget(widget); int targeItemIndex = 30; observerController.jumpTo(index: targeItemIndex); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observeResult?.firstChild?.index, targeItemIndex); targeItemIndex = 60; observerController.animateTo( index: targeItemIndex, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observeResult?.firstChild?.index, targeItemIndex); scrollController.dispose(); }); testWidgets('Fixed height with itemExtent', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); Widget widget = getFixedHeightListView( scrollController: scrollController, useItemExtentBuilder: false, ); ListViewObserveModel? observeResult; widget = ListViewObserver( child: widget, controller: observerController, onObserve: (result) { observeResult = result; }, ); await tester.pumpWidget(widget); int targeItemIndex = 30; observerController.jumpTo( index: targeItemIndex, isFixedHeight: true, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observeResult?.firstChild?.index, targeItemIndex); targeItemIndex = 60; observerController.animateTo( index: targeItemIndex, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, isFixedHeight: true, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observeResult?.firstChild?.index, targeItemIndex); scrollController.dispose(); }); testWidgets('Fixed height with itemExtentBuilder', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController(controller: scrollController); Widget widget = getFixedHeightListView( scrollController: scrollController, useItemExtentBuilder: true, ); ListViewObserveModel? observeResult; widget = ListViewObserver( child: widget, controller: observerController, onObserve: (result) { observeResult = result; }, ); await tester.pumpWidget(widget); int targeItemIndex = 30; observerController.jumpTo( index: targeItemIndex, isFixedHeight: true, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observeResult?.firstChild?.index, targeItemIndex); targeItemIndex = 60; observerController.animateTo( index: targeItemIndex, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, isFixedHeight: true, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observeResult?.firstChild?.index, targeItemIndex); scrollController.dispose(); }); // Regression test for https://github.com/fluttercandies/flutter_scrollview_observer/issues/123 testWidgets( 'No exception when ListViewObserverState is disposed during scrolling', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); Widget widget = getListView( scrollController: scrollController, ); widget = ListViewObserver( child: widget, controller: observerController, onObserve: (_) {}, ); await tester.pumpWidget(widget); observerController.animateTo( index: 10, duration: const Duration(seconds: 3), curve: Curves.easeInOut, ); await tester.pump(); await tester.pump(const Duration(seconds: 1)); await tester.pumpWidget(Container()); scrollController.dispose(); }); }); group('Cache index offset', () { testWidgets('Property cacheJumpIndexOffset', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController(controller: scrollController) ..cacheJumpIndexOffset = false; Widget widget = getListView( scrollController: scrollController, ); widget = ListViewObserver( child: widget, controller: observerController, ); await tester.pumpWidget(widget); observerController.jumpTo(index: 30); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observerController.indexOffsetMap.isEmpty, true); scrollController.dispose(); }); testWidgets('Method clearScrollIndexCache', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController(controller: scrollController); Widget widget = getListView( scrollController: scrollController, ); widget = ListViewObserver( child: widget, controller: observerController, ); await tester.pumpWidget(widget); final ctx = observerController.fetchSliverContext(); expect(ctx != null, true); observerController.jumpTo(index: 30); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observerController.indexOffsetMap[ctx]?.isNotEmpty, true); observerController.clearScrollIndexCache(); expect(observerController.indexOffsetMap[ctx]?.isEmpty, true); scrollController.dispose(); }); }); testWidgets('Check displayPercentage', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); Widget widget = getListView( scrollController: scrollController, ); ListViewObserveModel? observeResult; widget = ListViewObserver( child: widget, controller: observerController, onObserve: (result) { observeResult = result; }, ); await tester.pumpWidget(widget); int targetItemIndex = 30; observerController.jumpTo( index: targetItemIndex, alignment: 0, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); var firstChild = observeResult?.firstChild; expect(firstChild, isNotNull); expect(firstChild?.index, targetItemIndex); expect(firstChild?.displayPercentage, 1); observerController.jumpTo( index: targetItemIndex, alignment: 0.5, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); firstChild = observeResult?.firstChild; expect(firstChild?.index, targetItemIndex); expect(firstChild?.displayPercentage, 0.5); observerController.jumpTo( index: targetItemIndex, alignment: 1, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); firstChild = observeResult?.firstChild; expect(firstChild?.index, targetItemIndex + 1); scrollController.dispose(); }); testWidgets('Check observeIntervalForScrolling', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, )..observeIntervalForScrolling = const Duration(milliseconds: 500); int observeCount = 0; Widget widget = getListView( scrollController: scrollController, ); widget = ListViewObserver( child: widget, controller: observerController, autoTriggerObserveTypes: const [ ObserverAutoTriggerObserveType.scrollStart, ObserverAutoTriggerObserveType.scrollUpdate, ObserverAutoTriggerObserveType.scrollEnd, ], triggerOnObserveType: ObserverTriggerOnObserveType.directly, onObserve: (result) { observeCount++; }, ); await tester.pumpWidget(widget); final finder = find.byWidget(widget); await tester.timedDragFrom( tester.getCenter(finder), const Offset(0, -50), const Duration(milliseconds: 400), ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observeCount, 3); observeCount = 0; await tester.timedDragFrom( tester.getCenter(finder), const Offset(0, -50), const Duration(milliseconds: 600), ); await tester.pumpAndSettle(); await tester.pump(const Duration(milliseconds: 600)); expect(observeCount, 4); scrollController.dispose(); }); testWidgets('Check isForbidObserveCallback', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController(controller: scrollController); Widget widget = getListView( scrollController: scrollController, ); bool isCalledOnObserve = false; widget = ListViewObserver( child: widget, controller: observerController, onObserve: (result) { isCalledOnObserve = true; }, ); await tester.pumpWidget(widget); observerController.isForbidObserveCallback = true; observerController.jumpTo(index: 10); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserve, false); observerController.isForbidObserveCallback = false; observerController.jumpTo(index: 30); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserve, true); scrollController.dispose(); }); testWidgets('Check displayingChildModelMap', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); Widget widget = getListView( scrollController: scrollController, ); ListViewObserveModel? observeResult; widget = ListViewObserver( child: widget, controller: observerController, onObserve: (result) { observeResult = result; }, ); await tester.pumpWidget(widget); await observerController.dispatchOnceObserve( isForce: true, ); expect(observeResult, isNotNull); expect(observeResult?.firstChild?.index, 0); expect( listEquals( observeResult?.displayingChildModelMap.values.toList(), observeResult?.displayingChildModelList, ), true, ); int targetItemIndex = 30; observerController.jumpTo( index: targetItemIndex, alignment: 0, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observeResult?.firstChild?.index, targetItemIndex); expect( listEquals( observeResult?.displayingChildModelMap.values.toList(), observeResult?.displayingChildModelList, ), true, ); scrollController.dispose(); }); group( 'ObserverScrollNotification', () { late ScrollController scrollController; late ListObserverController observerController; late Widget widget; int indexOfStartNoti = -1; int indexOfInterruptionNoti = -1; int indexOfDecisionNoti = -1; int indexOfEndNoti = -1; resetAll({ bool isFixedHeight = false, }) { indexOfStartNoti = -1; indexOfInterruptionNoti = -1; indexOfDecisionNoti = -1; indexOfEndNoti = -1; scrollController = ScrollController(); observerController = ListObserverController( controller: scrollController, ); widget = getListView( scrollController: scrollController, itemCount: 100, isFixedHeight: isFixedHeight, ); widget = ListViewObserver( child: widget, controller: observerController, ); int count = 0; widget = NotificationListener( child: widget, onNotification: (notification) { if (notification is ObserverScrollStartNotification) { indexOfStartNoti = count; } else if (notification is ObserverScrollInterruptionNotification) { indexOfInterruptionNoti = count; } else if (notification is ObserverScrollDecisionNotification) { indexOfDecisionNoti = count; } else if (notification is ObserverScrollEndNotification) { indexOfEndNoti = count; } count += 1; return true; }, ); } tearDown(() { scrollController.dispose(); }); testWidgets( 'Notification sequence in normal scenarios', (tester) async { resetAll(); await tester.pumpWidget(widget); observerController.jumpTo(index: 10); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(indexOfStartNoti, 0); expect(indexOfInterruptionNoti, -1); expect(indexOfDecisionNoti, 1); expect(indexOfEndNoti, 2); }, ); testWidgets( 'Notification sequence in normal scenarios with fixed item height', (tester) async { resetAll(isFixedHeight: true); await tester.pumpWidget(widget); observerController.jumpTo(index: 10); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(indexOfStartNoti, 0); expect(indexOfInterruptionNoti, -1); expect(indexOfDecisionNoti, 1); expect(indexOfEndNoti, 2); }, ); testWidgets( 'Notification sequence when using incorrect index', (tester) async { resetAll(); await tester.pumpWidget(widget); observerController.jumpTo(index: 101); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(indexOfStartNoti, 0); expect(indexOfInterruptionNoti, 1); expect(indexOfDecisionNoti, -1); expect(indexOfEndNoti, -1); }, ); testWidgets( 'Notification sequence when using incorrect index with fixed item height', (tester) async { resetAll(isFixedHeight: true); await tester.pumpWidget(widget); observerController.jumpTo(index: 101); await tester.pumpAndSettle(); expect(indexOfStartNoti, 0); expect(indexOfInterruptionNoti, 1); expect(indexOfDecisionNoti, -1); expect(indexOfEndNoti, -1); }, ); }, ); group('dispatchOnceObserve', () { late ScrollController scrollController; late ListObserverController observerController; late Widget widget; tearDown(() { scrollController.dispose(); }); resetAll() { scrollController = ScrollController(); observerController = ListObserverController( controller: scrollController, ); widget = getListView( scrollController: scrollController, itemCount: 100, ); widget = ListViewObserver( child: widget, controller: observerController, ); } testWidgets( 'Check observeResult', (tester) async { resetAll(); await tester.pumpWidget(widget); var result = await observerController.dispatchOnceObserve(); expect(result.isSuccess, isFalse); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect(result.observeResult, isNotNull); expect( result.observeResult?.displayingChildIndexList ?? [], isNotEmpty, ); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect( result.observeResult?.displayingChildIndexList ?? [], isEmpty, ); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expect(result.isSuccess, isTrue); expect( result.observeResult?.displayingChildIndexList ?? [], isNotEmpty, ); }, ); testWidgets( 'Check observeAllResult', (tester) async { resetAll(); await tester.pumpWidget(widget); var result = await observerController.dispatchOnceObserve(); expect(result.isSuccess, isFalse); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect(result.observeAllResult.values.length, 1); expect( result.observeAllResult.values.first, result.observeResult, ); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect(result.observeAllResult, isEmpty); result = await observerController.dispatchOnceObserve( isDependObserveCallback: false, isForce: true, ); expect(result.isSuccess, isTrue); expect(result.observeAllResult, isNotEmpty); }, ); }); group('ObserverListener', () { late String tag1; late String tag2; late GlobalKey key1; late GlobalKey key2; late ScrollController scrollController; late ListObserverController observerController1; late ListObserverController observerController2; late Widget widget; tearDown(() { scrollController.dispose(); }); resetAll({ bool isResetTag = false, }) { if (isResetTag) { tag1 = tag1 * 2; tag2 = tag2 * 2; } else { tag1 = 'tag1'; tag2 = 'tag2'; key1 = GlobalKey(); key2 = GlobalKey(); scrollController = ScrollController(); observerController1 = ListObserverController( controller: scrollController, ); observerController2 = ListObserverController( controller: scrollController, ); } widget = getListView( scrollController: scrollController, itemCount: 100, itemBuilder: (context, index) { if (index == 3) { return const SizedBox( height: 50, child: Center( child: Icon(Icons.abc), ), ); } return SizedBox( height: 80, child: Center( child: Text("index -- $index"), ), ); }, ); widget = ListViewObserver( key: key2, tag: tag2, child: widget, controller: observerController2, ); widget = ListViewObserver( key: key1, tag: tag1, child: widget, controller: observerController1, ); } testWidgets('Change tag', (tester) async { resetAll(); await tester.pumpWidget(widget); final itemFinder = find.byIcon(Icons.abc); ObserverWidgetTagManager? tagManager = ObserverWidgetTagManager.maybeOf( tester.element(itemFinder), ); expect(tagManager, isNotNull); Map tagMap = Map.from(tagManager?.tagMap ?? {}); Set tags = {tag1, tag2}; expect(tagMap.keys.toSet(), tags); resetAll(isResetTag: true); await tester.pumpWidget(widget); tagManager = ObserverWidgetTagManager.maybeOf( tester.element(itemFinder), ); tagMap = Map.from(tagManager?.tagMap ?? {}); Set newTags = {tag1, tag2}; expect(tags != newTags, isTrue); expect(tagMap.keys.toSet(), newTags); }); testWidgets('of', (tester) async { resetAll(); await tester.pumpWidget(widget); final itemFinder = find.byIcon(Icons.abc); ListViewObserveModel? cbResult; Map? cbAllResult; onObserveCallback(ListViewObserveModel result) { cbResult = result; } onObserveAllCallback( Map resultMap, ) { cbAllResult = resultMap; } final observerState = ListViewObserver.of( tester.element(itemFinder), ); expect(observerState, isNotNull); final observerStateByTag2 = ListViewObserver.of( tester.element(itemFinder), tag: tag2, ); expect(observerStateByTag2 == key2.currentState, isTrue); expect(observerStateByTag2 == observerState, isTrue); final observerStateByTag1 = ListViewObserver.of( tester.element(itemFinder), tag: tag1, ); expect(observerStateByTag1 == key1.currentState, isTrue); expect(observerStateByTag1 == observerState, isFalse); observerState.addListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, ); expect(observerState.innerListeners?.length, 1); final result = await observerController2.dispatchOnceObserve(); expect(result.observeResult, cbResult); expect(result.observeAllResult, cbAllResult); observerState.removeListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, ); expect(observerState.innerListeners?.length, 0); }); testWidgets('maybeOf', (tester) async { resetAll(); await tester.pumpWidget(widget); final itemFinder = find.byIcon(Icons.abc); ListViewObserverState? observerState = ListViewObserver.maybeOf( tester.element(find.byKey(key1)), ); expect(observerState, isNull); ListViewObserveModel? cbResult; Map? cbAllResult; onObserveCallback(ListViewObserveModel result) { cbResult = result; } onObserveAllCallback( Map resultMap, ) { cbAllResult = resultMap; } observerState = ListViewObserver.maybeOf( tester.element(itemFinder), ); expect(observerState, isNotNull); final observerStateByTag2 = ListViewObserver.maybeOf( tester.element(itemFinder), tag: tag2, ); expect(observerStateByTag2 == key2.currentState, isTrue); expect(observerStateByTag2 == observerState, isTrue); final observerStateByTag1 = ListViewObserver.maybeOf( tester.element(itemFinder), tag: tag1, ); expect(observerStateByTag1 == key1.currentState, isTrue); expect(observerStateByTag1 == observerState, isFalse); observerState?.addListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, ); expect(observerState?.innerListeners?.length, 1); final result = await observerController2.dispatchOnceObserve(); expect(result.observeResult, cbResult); expect(result.observeAllResult, cbAllResult); observerState?.removeListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, ); expect(observerState?.innerListeners?.length, 0); }); testWidgets( 'innerTagChangeCount should not increase when tag remains unchanged', (tester) async { const String tag = 'tag1'; scrollController = ScrollController(); Widget listView = getListView(scrollController: scrollController); widget = ListViewObserver( tag: tag, child: listView, ); await tester.pumpWidget(widget); await tester.pumpAndSettle(); // Get ObserverWidgetState final itemFinder = find.byType(ListViewObserver); final observerState = tester.state(itemFinder); expect(observerState, isNotNull); // Record initial tagChangeCount final initialTagChangeCount = observerState.innerTagChangeCount; // Refresh widget but keep tag unchanged widget = ListViewObserver( tag: tag, child: listView, ); await tester.pumpWidget(widget); await tester.pumpAndSettle(); // Verify that tagChangeCount has not increased expect(observerState.innerTagChangeCount, initialTagChangeCount); expect(observerState.innerTagChangeCount, 0); }); testWidgets('No exception in _checkTagChange during refresh and dispose', (tester) async { // Regression test for https://github.com/fluttercandies/flutter_scrollview_observer/issues/143 scrollController = ScrollController(); Widget listView = getListView(scrollController: scrollController); widget = ListViewObserver( tag: 'tag1', child: listView, ); await tester.pumpWidget(widget); // Get ObserverWidgetState final itemFinder = find.byType(ListViewObserver); final observerState = tester.state(itemFinder); // Create a controllable Future final completer = Completer(); observerState.innerCheckTagChangeEndOfFrame = completer.future; widget = ListViewObserver( tag: 'tag2', child: listView, ); await tester.pumpWidget(widget); // Dispose widget before endOfFrame completes await tester.pumpWidget(Container()); // Complete Future to simulate endOfFrame callback execution // (widget has already been disposed at this point) completer.complete(); // Verify no exception is thrown await tester.pumpAndSettle(); }); }); } ================================================ FILE: test/observer_utils_test.dart ================================================ /* * @Author: LinXunFeng linxunfeng@yeah.net * @Repo: https://github.com/fluttercandies/flutter_scrollview_observer * @Date: 2024-05-20 22:19:27 */ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; void main() { testWidgets('check calcAnchorTabIndex', (tester) async { final scrollController = ScrollController(); final observerController = ListObserverController( controller: scrollController, ); ObserveModel? observeModel; Widget widget = Directionality( textDirection: TextDirection.ltr, child: ListView.separated( controller: scrollController, itemBuilder: (ctx, index) { return const SizedBox( width: double.infinity, height: 80, ); }, separatorBuilder: (ctx, index) { return const SizedBox(height: 10); }, itemCount: 100, ), ); widget = ListViewObserver( child: widget, controller: observerController, onObserve: (resultModel) => observeModel = resultModel, ); await tester.pumpWidget(widget); observerController.jumpTo(index: 5); await tester.pumpAndSettle(); expect(observeModel, isNotNull); List tabIndexes = [0, 5, 10]; int tabIndex = ObserverUtils.calcAnchorTabIndex( observeModel: observeModel!, tabIndexes: tabIndexes, currentTabIndex: 0, ); expect(tabIndex, 1); observerController.jumpTo(index: 9); await tester.pumpAndSettle(); tabIndex = ObserverUtils.calcAnchorTabIndex( observeModel: observeModel!, tabIndexes: tabIndexes, currentTabIndex: 1, ); expect(tabIndex, 1); scrollController.dispose(); }); testWidgets('check calcAnchorTabIndexForList', (tester) async { List tabIndexes = [0, 6, 9, 11, 12, 16]; // ====== exact match ====== // tabIndexes: [0, 6, 9, 11, 12, 16] // firstIndex: 12 // result: 4 (the index of 12 in tabIndexes) expect( ObserverUtils.calcAnchorTabIndexForList( firstIndex: 12, tabIndexes: tabIndexes, currentTabIndex: 0, ), 4, ); // ====== no exact match ====== // tabIndexes: [0, 6, 9, 11, 12, 16] // firstIndex: 10 // result: 2 (the index of 9 in tabIndexes) expect( ObserverUtils.calcAnchorTabIndexForList( firstIndex: 10, tabIndexes: tabIndexes, currentTabIndex: 0, ), 2, ); expect( ObserverUtils.calcAnchorTabIndexForList( firstIndex: 17, tabIndexes: tabIndexes, currentTabIndex: 0, ), 5, ); }); } ================================================ FILE: test/sliver_observer_test.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:scrollview_observer/scrollview_observer.dart'; import 'package:scrollview_observer/src/common/observer_widget_tag_manager.dart'; void main() { GlobalKey sliverListKey = GlobalKey(); GlobalKey sliverGridKey = GlobalKey(); BuildContext? _sliverListCtx; BuildContext? _sliverGridCtx; double calcPersistentHeaderExtent({ required double offset, required GlobalKey widgetKey, }) { return ObserverUtils.calcPersistentHeaderExtent( key: widgetKey, offset: offset, ); } Widget _buildDirectionality({ required Widget child, }) { return Directionality( textDirection: TextDirection.ltr, child: child, ); } Widget _buildSliverListView({ NullableIndexedWidgetBuilder? builder, }) { return SliverList( key: sliverListKey, delegate: SliverChildBuilderDelegate( (ctx, index) { _sliverListCtx ??= ctx; if (builder != null) { return builder(ctx, index); } return Container( height: (index % 2 == 0) ? 80 : 50, color: Colors.red, child: Center( child: Text("index -- $index"), ), ); }, childCount: 30, ), ); } Widget _buildSliverGridView({ NullableIndexedWidgetBuilder? builder, }) { return SliverGrid( key: sliverGridKey, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 2.0, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { _sliverGridCtx ??= context; if (builder != null) { return builder(context, index); } return Container( color: Colors.green, child: Center( child: Text('index -- $index'), ), ); }, childCount: 150, ), ); } Widget _buildScrollView({ ScrollController? scrollController, NullableIndexedWidgetBuilder? listItemBuilder, NullableIndexedWidgetBuilder? gridItemBuilder, }) { return _buildDirectionality( child: CustomScrollView( controller: scrollController, slivers: [ _buildSliverListView( builder: listItemBuilder, ), _buildSliverGridView( builder: gridItemBuilder, ), ], ), ); } tearDown(() { _sliverListCtx = null; _sliverGridCtx = null; }); testWidgets('Check isForbidObserveCallback', (tester) async { final scrollController = ScrollController(); final observerController = SliverObserverController( controller: scrollController, ); Widget widget = _buildScrollView( scrollController: scrollController, ); bool isCalledOnObserve = false; widget = SliverViewObserver( child: widget, controller: observerController, sliverContexts: () { return [ if (_sliverListCtx != null) _sliverListCtx!, if (_sliverGridCtx != null) _sliverGridCtx!, ]; }, onObserveAll: (result) { isCalledOnObserve = true; }, ); await tester.pumpWidget(widget); observerController.isForbidObserveCallback = true; observerController.jumpTo(index: 10, sliverContext: _sliverListCtx); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserve, false); observerController.isForbidObserveCallback = false; observerController.jumpTo(index: 20, sliverContext: _sliverListCtx); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserve, true); scrollController.dispose(); }); testWidgets('Check isForbidObserveViewportCallback', (tester) async { final scrollController = ScrollController(); final observerController = SliverObserverController( controller: scrollController, ); Widget widget = _buildScrollView( scrollController: scrollController, ); bool isCalledOnObserveViewport = false; widget = SliverViewObserver( child: widget, controller: observerController, sliverContexts: () { return [ if (sliverListKey.currentContext != null) sliverListKey.currentContext!, if (sliverGridKey.currentContext != null) sliverGridKey.currentContext!, ]; }, onObserveViewport: (result) { isCalledOnObserveViewport = true; }, ); await tester.pumpWidget(widget); observerController.isForbidObserveViewportCallback = true; observerController.jumpTo(index: 10, sliverContext: _sliverListCtx); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserveViewport, false); observerController.isForbidObserveViewportCallback = false; observerController.jumpTo(index: 20, sliverContext: _sliverGridCtx); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserveViewport, true); scrollController.dispose(); }); testWidgets('Check observeIntervalForScrolling', (tester) async { final scrollController = ScrollController(); final observerController = SliverObserverController( controller: scrollController, )..observeIntervalForScrolling = const Duration(milliseconds: 500); int observeCountForOnObserveAll = 0; int observeCountForOnObserveViewport = 0; Widget widget = _buildScrollView( scrollController: scrollController, ); widget = SliverViewObserver( child: widget, controller: observerController, sliverContexts: () { return [ if (sliverListKey.currentContext != null) sliverListKey.currentContext!, if (sliverGridKey.currentContext != null) sliverGridKey.currentContext!, ]; }, autoTriggerObserveTypes: const [ ObserverAutoTriggerObserveType.scrollStart, ObserverAutoTriggerObserveType.scrollUpdate, ObserverAutoTriggerObserveType.scrollEnd, ], triggerOnObserveType: ObserverTriggerOnObserveType.directly, onObserveAll: (_) { observeCountForOnObserveAll++; }, onObserveViewport: (_) { observeCountForOnObserveViewport++; }, ); await tester.pumpWidget(widget); final finder = find.byWidget(widget); await tester.timedDragFrom( tester.getCenter(finder), const Offset(0, -50), const Duration(milliseconds: 400), ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(observeCountForOnObserveAll, 3); expect(observeCountForOnObserveViewport, 3); observeCountForOnObserveAll = 0; observeCountForOnObserveViewport = 0; await tester.timedDragFrom( tester.getCenter(finder), const Offset(0, -50), const Duration(milliseconds: 600), ); await tester.pumpAndSettle(); await tester.pump(const Duration(milliseconds: 600)); expect(observeCountForOnObserveAll, 4); expect(observeCountForOnObserveViewport, 4); scrollController.dispose(); }); testWidgets('scrollNotificationPredicate', (tester) async { final scrollController = ScrollController(); final observerController = SliverObserverController( controller: scrollController, ); final pageController = PageController(); bool isCalledOnObserve = false; Widget widget = _buildScrollView( scrollController: scrollController, listItemBuilder: (context, index) { if (index == 0) { return SizedBox( height: 200, child: PageView.builder( scrollDirection: Axis.horizontal, controller: pageController, itemBuilder: (ctx, index) { return const SizedBox.expand(); }, itemCount: 10, ), ); } return const SizedBox(height: 80); }, ); widget = SliverViewObserver( child: widget, controller: observerController, scrollNotificationPredicate: defaultScrollNotificationPredicate, sliverContexts: () { return [ if (_sliverListCtx != null) _sliverListCtx!, if (_sliverGridCtx != null) _sliverGridCtx!, ]; }, onObserveAll: (result) { isCalledOnObserve = true; }, ); await tester.pumpWidget(widget); pageController.animateToPage( 3, duration: const Duration(milliseconds: 100), curve: Curves.easeInOut, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(pageController.page, 3); expect(isCalledOnObserve, isFalse); scrollController.animateTo( 10, duration: const Duration(milliseconds: 100), curve: Curves.easeInOut, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(isCalledOnObserve, isTrue); scrollController.dispose(); pageController.dispose(); }); group( 'ObserverScrollNotification', () { late ScrollController scrollController; late SliverObserverController observerController; late Widget widget; int indexOfStartNoti = -1; int indexOfInterruptionNoti = -1; int indexOfDecisionNoti = -1; int indexOfEndNoti = -1; resetAll({ bool isFixedHeight = false, }) { indexOfStartNoti = -1; indexOfInterruptionNoti = -1; indexOfDecisionNoti = -1; indexOfEndNoti = -1; scrollController = ScrollController(); observerController = SliverObserverController( controller: scrollController, ); widget = _buildScrollView( scrollController: scrollController, ); widget = SliverViewObserver( child: widget, controller: observerController, ); int count = 0; widget = NotificationListener( child: widget, onNotification: (notification) { if (notification is ObserverScrollStartNotification) { indexOfStartNoti = count; } else if (notification is ObserverScrollInterruptionNotification) { indexOfInterruptionNoti = count; } else if (notification is ObserverScrollDecisionNotification) { indexOfDecisionNoti = count; } else if (notification is ObserverScrollEndNotification) { indexOfEndNoti = count; } count += 1; return true; }, ); } tearDown(() { scrollController.dispose(); }); testWidgets( 'Notification sequence in normal scenarios', (tester) async { resetAll(); await tester.pumpWidget(widget); observerController.jumpTo(index: 10); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(indexOfStartNoti, 0); expect(indexOfInterruptionNoti, -1); expect(indexOfDecisionNoti, 1); expect(indexOfEndNoti, 2); }, ); testWidgets( 'Notification sequence when using incorrect index', (tester) async { resetAll(); await tester.pumpWidget(widget); observerController.jumpTo(index: 101); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); expect(indexOfStartNoti, 0); expect(indexOfInterruptionNoti, 1); expect(indexOfDecisionNoti, -1); expect(indexOfEndNoti, -1); }, ); }, ); group('dispatchOnceObserve', () { late ScrollController scrollController; late SliverObserverController observerController; late Widget widget; tearDown(() { scrollController.dispose(); }); resetAll() { scrollController = ScrollController(); observerController = SliverObserverController( controller: scrollController, ); widget = _buildScrollView( scrollController: scrollController, ); widget = SliverViewObserver( sliverContexts: () => [ if (_sliverListCtx != null) _sliverListCtx!, if (_sliverGridCtx != null) _sliverGridCtx! ], child: widget, controller: observerController, ); } testWidgets( 'Check observeAllResult', (tester) async { resetAll(); await tester.pumpWidget(widget); var result = await observerController.dispatchOnceObserve( sliverContext: _sliverListCtx!, ); expect(result.isSuccess, isFalse); result = await observerController.dispatchOnceObserve( sliverContext: _sliverListCtx!, isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect(result.observeAllResult[_sliverListCtx], isNotNull); expect( result.observeAllResult[_sliverListCtx]?.displayingChildIndexList ?? [], isNotEmpty, ); expect(result.observeAllResult[_sliverGridCtx], isNotNull); result = await observerController.dispatchOnceObserve( sliverContext: _sliverListCtx!, isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect( result.observeAllResult[_sliverListCtx]?.displayingChildIndexList ?? [], isEmpty, ); result = await observerController.dispatchOnceObserve( sliverContext: _sliverListCtx!, isDependObserveCallback: false, isForce: true, ); expect(result.isSuccess, isTrue); expect( result.observeAllResult[_sliverListCtx]?.displayingChildIndexList ?? [], isNotEmpty, ); }, ); testWidgets( 'Check observeViewportResultModel', (tester) async { resetAll(); await tester.pumpWidget(widget); var result = await observerController.dispatchOnceObserve( sliverContext: _sliverListCtx!, ); expect(result.isSuccess, isFalse); result = await observerController.dispatchOnceObserve( sliverContext: _sliverListCtx!, isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect( result.observeViewportResultModel?.firstChild.sliverContext, _sliverListCtx, ); expect( result.observeViewportResultModel?.displayingChildModelList ?? [], isNotEmpty, ); result = await observerController.dispatchOnceObserve( sliverContext: _sliverListCtx!, isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect( result.observeViewportResultModel?.displayingChildModelList ?? [], isEmpty, ); result = await observerController.dispatchOnceObserve( sliverContext: _sliverListCtx!, isDependObserveCallback: false, isForce: true, ); expect(result.isSuccess, isTrue); expect( result.observeViewportResultModel?.displayingChildModelList ?? [], isNotEmpty, ); expect(_sliverGridCtx, isNotNull); observerController.jumpTo( index: 0, sliverContext: _sliverGridCtx, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); result = await observerController.dispatchOnceObserve( sliverContext: _sliverGridCtx!, isDependObserveCallback: false, ); expect(result.isSuccess, isTrue); expect( result.observeViewportResultModel?.firstChild.sliverContext, _sliverGridCtx, ); }, ); }); group( 'NestedScrollView', () { late ScrollController outerScrollController; ScrollController? bodyScrollController; late SliverObserverController observerController; late Widget widget; BuildContext? _sliverHeaderListCtx; BuildContext? _sliverHeaderGridCtx; BuildContext? _sliverBodyListCtx; BuildContext? _sliverBodyGridCtx; GlobalKey appBarKey = GlobalKey(); GlobalKey nestedScrollViewKey = GlobalKey(); NestedScrollUtil? nestedScrollUtil; Map resultMap = {}; int sliverListItemCount = 30; int sliverGridItemCount = 150; Widget _buildSliverGridView() { return SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 2.0, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { if (_sliverBodyGridCtx != context) { _sliverBodyGridCtx = context; nestedScrollUtil?.bodySliverContexts.add(context); } return Container( color: Colors.green, child: Center( child: Text('index -- $index'), ), ); }, childCount: sliverGridItemCount, ), ); } Widget _buildSliverListView() { return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { if (_sliverBodyListCtx != ctx) { _sliverBodyListCtx = ctx; nestedScrollUtil?.bodySliverContexts.add(ctx); } return Container( height: (index % 2 == 0) ? 80 : 50, color: Colors.red, child: Center( child: Text( "index -- $index", style: const TextStyle(color: Colors.white), ), ), ); }, childCount: sliverListItemCount, ), ); } Widget _buildNestedScrollView() { return NestedScrollView( key: nestedScrollViewKey, controller: outerScrollController, headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverAppBar( key: appBarKey, title: const Text("NestedScrollView"), pinned: true, forceElevated: innerBoxIsScrolled, ), SliverFixedExtentList( delegate: SliverChildBuilderDelegate( (ctx, index) { if (_sliverHeaderListCtx != ctx) { _sliverHeaderListCtx = ctx; nestedScrollUtil?.headerSliverContexts.add(ctx); } return ListTile( leading: Text("Item $index"), ); }, childCount: 5, ), itemExtent: 50, ), SliverGrid.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 2.0, ), itemBuilder: (context, index) { if (_sliverHeaderGridCtx != context) { _sliverHeaderGridCtx = context; nestedScrollUtil?.headerSliverContexts.add(context); } return Text("Item $index"); }, itemCount: 10, ), ]; }, body: Builder(builder: (context) { final innerScrollController = PrimaryScrollController.of(context); if (bodyScrollController != innerScrollController) { bodyScrollController = innerScrollController; nestedScrollUtil?.bodyScrollController = innerScrollController; nestedScrollUtil?.outerScrollController = outerScrollController; } return CustomScrollView( slivers: [ _buildSliverListView(), _buildSliverGridView(), ], ); }), ); } Widget resetAll({ int listItemCount = 30, int gridItemCount = 150, }) { sliverListItemCount = listItemCount; sliverGridItemCount = gridItemCount; resultMap = {}; nestedScrollUtil = NestedScrollUtil(); outerScrollController = ScrollController(); bodyScrollController = null; observerController = SliverObserverController( controller: outerScrollController, ); widget = _buildNestedScrollView(); widget = SliverViewObserver( controller: observerController, child: widget, sliverContexts: () { return [ if (_sliverHeaderListCtx != null) _sliverHeaderListCtx!, if (_sliverHeaderGridCtx != null) _sliverHeaderGridCtx!, if (_sliverBodyListCtx != null) _sliverBodyListCtx!, if (_sliverBodyGridCtx != null) _sliverBodyGridCtx!, ]; }, customOverlap: (sliverContext) { return nestedScrollUtil?.calcOverlap( nestedScrollViewKey: nestedScrollViewKey, sliverContext: sliverContext, ); }, onObserveAll: (result) { resultMap = result; }, ); widget = MaterialApp( home: Material(child: widget), ); return widget; } tearDown(() { outerScrollController.dispose(); _sliverHeaderListCtx = null; _sliverHeaderGridCtx = null; _sliverBodyListCtx = null; _sliverBodyGridCtx = null; }); testWidgets( 'Scroll to index', (tester) async { resetAll(); await tester.pumpWidget(widget); observerController.controller = outerScrollController; nestedScrollUtil?.jumpTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverHeaderListCtx, position: NestedScrollUtilPosition.header, index: 1, offset: (targetOffset) { return calcPersistentHeaderExtent( offset: targetOffset, widgetKey: appBarKey, ); }, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); var headerListObservationResult = (resultMap[_sliverHeaderListCtx] as ListViewObserveModel); expect(headerListObservationResult.firstChild?.index, 1); observerController.controller = outerScrollController; nestedScrollUtil?.animateTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverHeaderListCtx, position: NestedScrollUtilPosition.header, index: 2, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, offset: (targetOffset) { return calcPersistentHeaderExtent( offset: targetOffset, widgetKey: appBarKey, ); }, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); headerListObservationResult = (resultMap[_sliverHeaderListCtx] as ListViewObserveModel); expect(headerListObservationResult.firstChild?.index, 2); expect(bodyScrollController != null, true); nestedScrollUtil?.jumpTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverBodyListCtx, position: NestedScrollUtilPosition.body, index: 5, offset: (targetOffset) { return calcPersistentHeaderExtent( offset: targetOffset, widgetKey: appBarKey, ); }, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); var bodyListObservationResult = (resultMap[_sliverBodyListCtx] as ListViewObserveModel); expect(bodyListObservationResult.firstChild?.index, 5); nestedScrollUtil?.animateTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverBodyListCtx, position: NestedScrollUtilPosition.body, index: 20, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, offset: (targetOffset) { return calcPersistentHeaderExtent( offset: targetOffset, widgetKey: appBarKey, ); }, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); bodyListObservationResult = (resultMap[_sliverBodyListCtx] as ListViewObserveModel); expect(bodyListObservationResult.firstChild?.index, 20); nestedScrollUtil?.jumpTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverBodyGridCtx, position: NestedScrollUtilPosition.body, index: 10, offset: (targetOffset) { return calcPersistentHeaderExtent( offset: targetOffset, widgetKey: appBarKey, ); }, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); var bodyGridObservationResult = (resultMap[_sliverBodyGridCtx] as GridViewObserveModel); var bodyGridFirstGroupChildIndexList = bodyGridObservationResult .firstGroupChildList .map((e) => e.index) .toList(); expect(bodyGridFirstGroupChildIndexList.contains(10), true); nestedScrollUtil?.animateTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverBodyGridCtx, position: NestedScrollUtilPosition.body, index: 20, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, offset: (targetOffset) { return calcPersistentHeaderExtent( offset: targetOffset, widgetKey: appBarKey, ); }, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); bodyGridObservationResult = (resultMap[_sliverBodyGridCtx] as GridViewObserveModel); bodyGridFirstGroupChildIndexList = bodyGridObservationResult .firstGroupChildList .map((e) => e.index) .toList(); expect(bodyGridFirstGroupChildIndexList.contains(20), true); }, ); testWidgets( 'Check method reset of NestedScrollUtil', (tester) async { resetAll(); await tester.pumpWidget(widget); expect(nestedScrollUtil?.headerSliverContexts.length, 2); expect(nestedScrollUtil?.bodySliverContexts.length, 2); expect(nestedScrollUtil?.remainingSliverContext == null, true); expect(nestedScrollUtil?.remainingSliverRenderObj == null, true); nestedScrollUtil?.fetchRemainingSliverContext( nestedScrollViewKey: nestedScrollViewKey, ); expect(nestedScrollUtil?.remainingSliverContext != null, true); expect(nestedScrollUtil?.remainingSliverRenderObj != null, true); nestedScrollUtil?.reset(); expect(nestedScrollUtil?.headerSliverContexts.length, 0); expect(nestedScrollUtil?.bodySliverContexts.length, 0); expect(nestedScrollUtil?.remainingSliverContext, null); expect(nestedScrollUtil?.remainingSliverRenderObj, null); }, ); testWidgets( 'Check the observed data when the sliver in the header is not visible', (tester) async { resetAll(); await tester.pumpWidget(widget); await observerController.dispatchOnceObserve( sliverContext: _sliverHeaderListCtx!, ); var headerListObservationResult = (resultMap[_sliverHeaderListCtx] as ListViewObserveModel); expect( headerListObservationResult.displayingChildIndexList, isNotEmpty, ); nestedScrollUtil?.jumpTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverHeaderGridCtx, position: NestedScrollUtilPosition.header, index: 0, offset: (targetOffset) { return calcPersistentHeaderExtent( offset: targetOffset, widgetKey: appBarKey, ); }, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); headerListObservationResult = (resultMap[_sliverHeaderListCtx] as ListViewObserveModel); expect( headerListObservationResult.displayingChildIndexList, isEmpty, ); var headerGridObservationResult = (resultMap[_sliverHeaderGridCtx] as GridViewObserveModel); expect( headerGridObservationResult.firstGroupChildList.first.index, 0, ); nestedScrollUtil?.jumpTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverBodyListCtx, position: NestedScrollUtilPosition.body, index: 0, offset: (targetOffset) { return calcPersistentHeaderExtent( offset: targetOffset, widgetKey: appBarKey, ); }, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); var bodyListObservationResult = (resultMap[_sliverBodyListCtx] as ListViewObserveModel); expect( bodyListObservationResult.firstChild?.index, 0, ); headerGridObservationResult = (resultMap[_sliverHeaderGridCtx] as GridViewObserveModel); expect( headerGridObservationResult.displayingChildIndexList, isEmpty, ); }, ); testWidgets('Check animateTo Future completion', (tester) async { resetAll(); await tester.pumpWidget(widget); bool isFutureCompleted = false; const Duration duration = Duration(seconds: 2); // Start to scroll Future future = nestedScrollUtil!.animateTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverBodyListCtx, position: NestedScrollUtilPosition.body, index: 5, duration: duration, curve: Curves.linear, ); future.whenComplete(() { isFutureCompleted = true; }); // Wait for scroll to start await tester.pump(); // Verify scroll hasn't completed expect(isFutureCompleted, false); // Wait for scroll to complete const intervalDuration = Duration(milliseconds: 100); int intervalTime = 0; while (tester.binding.hasScheduledFrame) { await tester.pump(intervalDuration); intervalTime++; } // Make sure the scroll executes and completes correctly. expect( intervalTime, greaterThanOrEqualTo( duration.inMilliseconds ~/ intervalDuration.inMilliseconds, ), ); expect(isFutureCompleted, true); // Scroll back to index 0 isFutureCompleted = false; intervalTime = 0; future = nestedScrollUtil!.animateTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverBodyListCtx, position: NestedScrollUtilPosition.body, index: 0, duration: duration, curve: Curves.easeInOut, ); future.whenComplete(() { isFutureCompleted = true; }); // Wait for scroll to start await tester.pump(); // Verify scroll hasn't completed expect(isFutureCompleted, false); // Wait for scroll to complete while (tester.binding.hasScheduledFrame) { await tester.pump(intervalDuration); intervalTime++; } // Make sure the scroll executes and completes correctly. expect( intervalTime, greaterThanOrEqualTo( duration.inMilliseconds ~/ intervalDuration.inMilliseconds, ), ); expect(isFutureCompleted, true); }); testWidgets('Check jumpTo Future completion', (tester) async { int listItemCount = 100; resetAll( listItemCount: listItemCount, ); await tester.pumpWidget(widget); bool isFutureCompleted = false; // Start to scroll Future future = nestedScrollUtil!.jumpTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverBodyListCtx, position: NestedScrollUtilPosition.body, // Need to use an unrendered item's index to force paging to find it. index: listItemCount - 1, ); future.whenComplete(() { isFutureCompleted = true; }); // Wait for scroll to start await tester.pump(); // Verify scroll hasn't completed expect(isFutureCompleted, false); // Wait for scroll to complete const intervalDuration = Duration(milliseconds: 100); int intervalTime = 0; while (tester.binding.hasScheduledFrame) { await tester.pump(intervalDuration); intervalTime++; } // Make sure the scroll executes and completes correctly. expect( intervalTime, greaterThanOrEqualTo(0), ); expect(isFutureCompleted, true); // Scroll back to index 0 isFutureCompleted = false; intervalTime = 0; future = nestedScrollUtil!.jumpTo( nestedScrollViewKey: nestedScrollViewKey, observerController: observerController, sliverContext: _sliverBodyListCtx, position: NestedScrollUtilPosition.body, index: 0, ); future.whenComplete(() { isFutureCompleted = true; }); // Wait for scroll to start await tester.pump(); // Verify scroll hasn't completed expect(isFutureCompleted, false); // Wait for scroll to complete while (tester.binding.hasScheduledFrame) { await tester.pump(intervalDuration); intervalTime++; } // Make sure the scroll executes and completes correctly. expect( intervalTime, greaterThanOrEqualTo(0), ); expect(isFutureCompleted, true); }); }, ); group( 'Configure center in CustomScrollView', () { late Widget widget; BuildContext? _sliverListCtx1; BuildContext? _sliverListCtx2; BuildContext? _sliverListCtx3; BuildContext? _sliverListCtx4; final _centerKey = GlobalKey(); ScrollController scrollController = ScrollController(); late SliverObserverController observerController; Map resultMap = {}; Widget _buildSliverListView({ required Color color, Function(BuildContext)? onBuild, }) { return SliverList( delegate: SliverChildBuilderDelegate( (ctx, index) { onBuild?.call(ctx); final int itemIndex = index ~/ 2; return Container( height: (itemIndex % 2 == 0) ? 80 : 50, color: color, child: Center( child: Text( "index -- $index", ), ), ); }, childCount: 100, ), ); } Widget _buildScrollView({ required double anchor, }) { return CustomScrollView( center: _centerKey, anchor: anchor, controller: scrollController, slivers: [ _buildSliverListView( color: Colors.redAccent, onBuild: (ctx) { _sliverListCtx1 = ctx; }, ), _buildSliverListView( color: Colors.blueGrey, onBuild: (ctx) { _sliverListCtx2 = ctx; }, ), SliverPadding(padding: EdgeInsets.zero, key: _centerKey), _buildSliverListView( color: Colors.teal, onBuild: (ctx) { _sliverListCtx3 = ctx; }, ), _buildSliverListView( color: Colors.purple, onBuild: (ctx) { _sliverListCtx4 = ctx; }, ), ], ); } Widget resetAll({ required double anchor, }) { resultMap = {}; scrollController = ScrollController(); observerController = SliverObserverController( controller: scrollController, ); widget = _buildScrollView(anchor: anchor); widget = SliverViewObserver( controller: observerController, child: widget, sliverContexts: () { return [ if (_sliverListCtx1 != null) _sliverListCtx1!, if (_sliverListCtx2 != null) _sliverListCtx2!, if (_sliverListCtx3 != null) _sliverListCtx3!, if (_sliverListCtx4 != null) _sliverListCtx4!, ]; }, onObserveAll: (result) { resultMap = result; }, ); widget = MaterialApp( home: Material(child: widget), ); return widget; } tearDown(() { scrollController.dispose(); _sliverListCtx1 = null; _sliverListCtx2 = null; _sliverListCtx3 = null; _sliverListCtx4 = null; }); testWidgets('Check isForwardGrowthDirection', (tester) async { resetAll(anchor: 0.5); await tester.pumpWidget(widget); final _sliverListObj1 = ObserverUtils.findRenderObject(_sliverListCtx1); expect(_sliverListObj1 is RenderSliverMultiBoxAdaptor, true); _sliverListObj1 as RenderSliverMultiBoxAdaptor; expect(_sliverListObj1.isForwardGrowthDirection, false); final _sliverListObj2 = ObserverUtils.findRenderObject(_sliverListCtx2); expect(_sliverListObj2 is RenderSliverMultiBoxAdaptor, true); _sliverListObj2 as RenderSliverMultiBoxAdaptor; expect(_sliverListObj2.isForwardGrowthDirection, false); final _sliverListObj3 = ObserverUtils.findRenderObject(_sliverListCtx3); expect(_sliverListObj3 is RenderSliverMultiBoxAdaptor, true); _sliverListObj3 as RenderSliverMultiBoxAdaptor; expect(_sliverListObj3.isForwardGrowthDirection, true); final _sliverListObj4 = ObserverUtils.findRenderObject(_sliverListCtx4); expect(_sliverListObj4 is RenderSliverMultiBoxAdaptor, true); _sliverListObj4 as RenderSliverMultiBoxAdaptor; expect(_sliverListObj4.isForwardGrowthDirection, true); }); testWidgets('Check viewportExtremeScrollExtent and rectify', (tester) async { resetAll(anchor: 0.5); await tester.pumpWidget(widget); final _sliverListObj1 = ObserverUtils.findRenderObject(_sliverListCtx1); expect(_sliverListObj1 != null, true); expect(_sliverListObj1 is RenderSliverMultiBoxAdaptor, true); _sliverListObj1 as RenderSliverMultiBoxAdaptor; var extremeScrollExtent = observerController.viewportExtremeScrollExtent( viewport: ObserverUtils.findViewport(_sliverListObj1)!, obj: _sliverListObj1, ); expect(extremeScrollExtent <= 0, true); expect(extremeScrollExtent.rectify(_sliverListObj1) >= 0, true); final _sliverListObj2 = ObserverUtils.findRenderObject(_sliverListCtx2); expect(_sliverListObj2 != null, true); expect(_sliverListObj2 is RenderSliverMultiBoxAdaptor, true); _sliverListObj2 as RenderSliverMultiBoxAdaptor; extremeScrollExtent = observerController.viewportExtremeScrollExtent( viewport: ObserverUtils.findViewport(_sliverListObj2)!, obj: _sliverListObj2, ); expect(extremeScrollExtent <= 0, true); expect(extremeScrollExtent.rectify(_sliverListObj2) >= 0, true); final _sliverListObj3 = ObserverUtils.findRenderObject(_sliverListCtx3); expect(_sliverListObj3 != null, true); expect(_sliverListObj3 is RenderSliverMultiBoxAdaptor, true); _sliverListObj3 as RenderSliverMultiBoxAdaptor; extremeScrollExtent = observerController.viewportExtremeScrollExtent( viewport: ObserverUtils.findViewport(_sliverListObj3)!, obj: _sliverListObj3, ); expect(extremeScrollExtent >= 0, true); expect(extremeScrollExtent.rectify(_sliverListObj3) >= 0, true); final _sliverListObj4 = ObserverUtils.findRenderObject(_sliverListCtx4); expect(_sliverListObj4 != null, true); expect(_sliverListObj4 is RenderSliverMultiBoxAdaptor, true); _sliverListObj4 as RenderSliverMultiBoxAdaptor; extremeScrollExtent = observerController.viewportExtremeScrollExtent( viewport: ObserverUtils.findViewport(_sliverListObj4)!, obj: _sliverListObj4, ); expect(extremeScrollExtent >= 0, true); expect(extremeScrollExtent.rectify(_sliverListObj4) >= 0, true); }); testWidgets( 'Scroll to index with anchor 1.0', (tester) async { resetAll(anchor: 1.0); await tester.pumpWidget(widget); observerController.jumpTo( index: 1, sliverContext: _sliverListCtx1, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); final sliverList1ObservationResult = (resultMap[_sliverListCtx1] as ListViewObserveModel); expect(sliverList1ObservationResult.firstChild?.index, 1); observerController.jumpTo( index: 5, sliverContext: _sliverListCtx2, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); final sliverList2ObservationResult = (resultMap[_sliverListCtx2] as ListViewObserveModel); expect(sliverList2ObservationResult.firstChild?.index, 5); observerController.jumpTo( index: 10, sliverContext: _sliverListCtx3, alignment: 1, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); final sliverList3ObservationResult = (resultMap[_sliverListCtx3] as ListViewObserveModel); expect( sliverList3ObservationResult.displayingChildModelList.last.index, 10, ); observerController.jumpTo( index: 8, sliverContext: _sliverListCtx4, alignment: 1, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); final sliverList4ObservationResult = (resultMap[_sliverListCtx4] as ListViewObserveModel); expect( sliverList4ObservationResult.displayingChildModelList.last.index, 8, ); }, ); testWidgets( 'Scroll to index with anchor 0.0', (tester) async { resetAll(anchor: 0.0); await tester.pumpWidget(widget); observerController.jumpTo( index: 1, sliverContext: _sliverListCtx1, alignment: 1, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); final sliverList1ObservationResult = (resultMap[_sliverListCtx1] as ListViewObserveModel); expect( sliverList1ObservationResult.displayingChildModelList.last.index, 1, ); observerController.jumpTo( index: 5, sliverContext: _sliverListCtx2, alignment: 1, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); final sliverList2ObservationResult = (resultMap[_sliverListCtx2] as ListViewObserveModel); expect( sliverList2ObservationResult.displayingChildModelList.last.index, 5, ); observerController.jumpTo( index: 10, sliverContext: _sliverListCtx3, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); final sliverList3ObservationResult = (resultMap[_sliverListCtx3] as ListViewObserveModel); expect(sliverList3ObservationResult.firstChild?.index, 10); observerController.jumpTo( index: 8, sliverContext: _sliverListCtx4, ); await tester.pumpAndSettle(); await tester.pump(observerController.observeIntervalForScrolling); final sliverList4ObservationResult = (resultMap[_sliverListCtx4] as ListViewObserveModel); expect(sliverList4ObservationResult.firstChild?.index, 8); }, ); }, ); group('ObserverListener', () { late String tag1; late String tag2; late GlobalKey key1; late GlobalKey key2; late ScrollController scrollController; late SliverObserverController observerController1; late SliverObserverController observerController2; late Widget widget; tearDown(() { scrollController.dispose(); }); resetAll({ bool isResetTag = false, }) { if (isResetTag) { tag1 = tag1 * 2; tag2 = tag2 * 2; } else { tag1 = 'tag1'; tag2 = 'tag2'; key1 = GlobalKey(); key2 = GlobalKey(); scrollController = ScrollController(); observerController1 = SliverObserverController( controller: scrollController, ); observerController2 = SliverObserverController( controller: scrollController, ); } widget = _buildScrollView( scrollController: scrollController, listItemBuilder: (context, index) { if (index == 3) { return const SizedBox( height: 50, child: Center( child: Icon(Icons.list), ), ); } return Container(height: 50); }, gridItemBuilder: (context, index) { if (index == 3) { return const Center( child: Icon(Icons.grid_view), ); } return Container(); }, ); widget = SliverViewObserver( key: key2, tag: tag2, sliverContexts: () => [ if (_sliverListCtx != null) _sliverListCtx!, if (_sliverGridCtx != null) _sliverGridCtx! ], child: widget, controller: observerController2, ); widget = SliverViewObserver( key: key1, tag: tag1, sliverContexts: () => [ if (_sliverListCtx != null) _sliverListCtx!, if (_sliverGridCtx != null) _sliverGridCtx! ], child: widget, controller: observerController1, ); } testWidgets('Change tag', (tester) async { resetAll(); await tester.pumpWidget(widget); final listItemFinder = find.byIcon(Icons.list); ObserverWidgetTagManager? tagManager = ObserverWidgetTagManager.maybeOf( tester.element(listItemFinder), ); expect(tagManager, isNotNull); Map tagMap = Map.from(tagManager?.tagMap ?? {}); Set tags = {tag1, tag2}; expect(tagMap.keys.toSet(), tags); resetAll(isResetTag: true); await tester.pumpWidget(widget); tagManager = ObserverWidgetTagManager.maybeOf( tester.element(listItemFinder), ); tagMap = Map.from(tagManager?.tagMap ?? {}); Set newTags = {tag1, tag2}; expect(tags != newTags, isTrue); expect(tagMap.keys.toSet(), newTags); }); testWidgets('of', (tester) async { resetAll(); await tester.pumpWidget(widget); final listItemFinder = find.byIcon(Icons.list); final gridItemFinder = find.byIcon(Icons.grid_view); ObserveModel? cbResult; Map? cbAllResult; SliverViewportObserveModel? cbObserveViewportResult; onObserveCallback(ObserveModel result) { cbResult = result; } onObserveAllCallback( Map resultMap, ) { cbAllResult = resultMap; } onObserveViewportCallback(SliverViewportObserveModel result) { cbObserveViewportResult = result; } final listObserverState = SliverViewObserver.of( tester.element(listItemFinder), ); expect(listObserverState, isNotNull); final listObserverStateByTag2 = SliverViewObserver.of( tester.element(listItemFinder), tag: tag2, ); expect(listObserverStateByTag2 == key2.currentState, isTrue); expect(listObserverStateByTag2 == listObserverState, isTrue); final listObserverStateByTag1 = SliverViewObserver.of( tester.element(listItemFinder), tag: tag1, ); expect(listObserverStateByTag1 == key1.currentState, isTrue); expect(listObserverStateByTag1 == listObserverState, isFalse); listObserverState.addListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, onObserveViewport: onObserveViewportCallback, ); expect(listObserverState.innerListeners?.length, 1); ScrollViewOnceObserveNotificationResult? result = await observerController2.dispatchOnceObserve( sliverContext: _sliverListCtx!, isDependObserveCallback: false, ); expect(result.observeResult, cbResult); expect(result.observeAllResult, cbAllResult); expect(result.observeViewportResultModel, cbObserveViewportResult); listObserverState.removeListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, onObserveViewport: onObserveViewportCallback, ); expect(listObserverState.innerListeners?.length, 0); observerController2.jumpTo( index: 0, sliverContext: _sliverGridCtx, ); await tester.pumpAndSettle(); await tester.pump(observerController2.observeIntervalForScrolling); cbResult = null; cbAllResult = null; cbObserveViewportResult = null; final gridObserverState = SliverViewObserver.of( tester.element(gridItemFinder), ); expect(gridObserverState, isNotNull); expect(gridObserverState == listObserverState, isTrue); final gridObserverStateByTag2 = SliverViewObserver.of( tester.element(gridItemFinder), tag: tag2, ); expect(gridObserverStateByTag2 == key2.currentState, isTrue); expect(gridObserverStateByTag2 == listObserverState, isTrue); final gridObserverStateByTag1 = SliverViewObserver.of( tester.element(gridItemFinder), tag: tag1, ); expect(gridObserverStateByTag1 == key1.currentState, isTrue); expect(gridObserverStateByTag1 == listObserverState, isFalse); gridObserverState.addListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, onObserveViewport: onObserveViewportCallback, ); expect(gridObserverState.innerListeners?.length, 1); result = await observerController2.dispatchOnceObserve( sliverContext: _sliverListCtx!, ); expect(result.observeResult, cbResult); expect(result.observeAllResult, cbAllResult); expect(result.observeViewportResultModel, cbObserveViewportResult); gridObserverState.removeListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, onObserveViewport: onObserveViewportCallback, ); expect(gridObserverState.innerListeners?.length, 0); }); testWidgets('maybeOf', (tester) async { resetAll(); await tester.pumpWidget(widget); final listItemFinder = find.byIcon(Icons.list); final gridItemFinder = find.byIcon(Icons.grid_view); MixViewObserverState? observerState = SliverViewObserver.maybeOf( tester.element(find.byKey(key1)), ); expect(observerState, isNull); ObserveModel? cbResult; Map? cbAllResult; SliverViewportObserveModel? cbObserveViewportResult; onObserveCallback(ObserveModel result) { cbResult = result; } onObserveAllCallback( Map resultMap, ) { cbAllResult = resultMap; } onObserveViewportCallback(SliverViewportObserveModel result) { cbObserveViewportResult = result; } MixViewObserverState? listObserverState = SliverViewObserver.maybeOf( tester.element(listItemFinder), ); expect(listObserverState, isNotNull); final listObserverStateByTag2 = SliverViewObserver.maybeOf( tester.element(listItemFinder), tag: tag2, ); expect(listObserverStateByTag2 == key2.currentState, isTrue); expect(listObserverStateByTag2 == listObserverState, isTrue); final listObserverStateByTag1 = SliverViewObserver.maybeOf( tester.element(listItemFinder), tag: tag1, ); expect(listObserverStateByTag1 == key1.currentState, isTrue); expect(listObserverStateByTag1 == listObserverState, isFalse); listObserverState?.addListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, onObserveViewport: onObserveViewportCallback, ); expect(listObserverState?.innerSliverListeners?.length, 1); expect(listObserverState?.innerListeners?.length, 1); ScrollViewOnceObserveNotificationResult? result = await observerController2.dispatchOnceObserve( sliverContext: _sliverListCtx!, isDependObserveCallback: false, ); expect(result.observeResult, cbResult); expect(result.observeAllResult, cbAllResult); expect(result.observeViewportResultModel, cbObserveViewportResult); listObserverState?.removeListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, onObserveViewport: onObserveViewportCallback, ); expect(listObserverState?.innerSliverListeners?.length, 0); expect(listObserverState?.innerListeners?.length, 0); observerController2.jumpTo( index: 0, sliverContext: _sliverGridCtx, ); await tester.pumpAndSettle(); await tester.pump(observerController2.observeIntervalForScrolling); cbResult = null; cbAllResult = null; cbObserveViewportResult = null; MixViewObserverState? gridObserverState = SliverViewObserver.maybeOf( tester.element(find.byIcon(Icons.grid_view)), ); expect(gridObserverState, isNotNull); final gridObserverStateByTag2 = SliverViewObserver.maybeOf( tester.element(gridItemFinder), tag: tag2, ); expect(gridObserverStateByTag2 == key2.currentState, isTrue); expect(gridObserverStateByTag2 == gridObserverState, isTrue); final gridObserverStateByTag1 = SliverViewObserver.maybeOf( tester.element(gridItemFinder), tag: tag1, ); expect(gridObserverStateByTag1 == key1.currentState, isTrue); expect(gridObserverStateByTag1 == gridObserverState, isFalse); gridObserverState?.addListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, onObserveViewport: onObserveViewportCallback, ); expect(gridObserverState?.innerSliverListeners?.length, 1); expect(gridObserverState?.innerListeners?.length, 1); result = await observerController2.dispatchOnceObserve( sliverContext: _sliverListCtx!, isDependObserveCallback: false, ); expect(result.observeResult, cbResult); expect(result.observeAllResult, cbAllResult); expect(result.observeViewportResultModel, cbObserveViewportResult); gridObserverState?.removeListener( onObserve: onObserveCallback, onObserveAll: onObserveAllCallback, onObserveViewport: onObserveViewportCallback, ); expect(gridObserverState?.innerSliverListeners?.length, 0); expect(gridObserverState?.innerListeners?.length, 0); }); // Regression test for https://github.com/fluttercandies/flutter_scrollview_observer/issues/120 testWidgets( 'No exception when MixViewObserverState is disposed during scrolling', (tester) async { resetAll(); await tester.pumpWidget(widget); observerController1.animateTo( index: 60, sliverContext: _sliverListCtx, duration: const Duration(seconds: 3), curve: Curves.easeInOut, ); await tester.pump(); await tester.pump(const Duration(seconds: 1)); await tester.pumpWidget(Container()); }); testWidgets( 'innerTagChangeCount should not increase when tag remains unchanged', (tester) async { const String tag = 'tag1'; scrollController = ScrollController(); Widget scrollView = _buildScrollView(scrollController: scrollController); widget = SliverViewObserver( tag: tag, child: scrollView, ); await tester.pumpWidget(widget); await tester.pumpAndSettle(); // Get ObserverWidgetState final itemFinder = find.byType(SliverViewObserver); final observerState = tester.state(itemFinder); expect(observerState, isNotNull); // Record initial tagChangeCount final initialTagChangeCount = observerState.innerTagChangeCount; // Refresh widget but keep tag unchanged widget = SliverViewObserver( tag: tag, child: scrollView, ); await tester.pumpWidget(widget); await tester.pumpAndSettle(); // Verify that tagChangeCount has not increased expect(observerState.innerTagChangeCount, initialTagChangeCount); expect(observerState.innerTagChangeCount, 0); }); testWidgets('No exception in _checkTagChange during refresh and dispose', (tester) async { // Regression test for https://github.com/fluttercandies/flutter_scrollview_observer/issues/143 scrollController = ScrollController(); Widget scrollView = _buildScrollView(scrollController: scrollController); widget = SliverViewObserver( tag: 'tag1', child: scrollView, ); await tester.pumpWidget(widget); // Get ObserverWidgetState final itemFinder = find.byType(SliverViewObserver); final observerState = tester.state(itemFinder); // Create a controllable Future final completer = Completer(); observerState.innerCheckTagChangeEndOfFrame = completer.future; widget = SliverViewObserver( tag: 'tag2', child: scrollView, ); await tester.pumpWidget(widget); // Dispose widget before endOfFrame completes await tester.pumpWidget(Container()); // Complete Future to simulate endOfFrame callback execution // (widget has already been disposed at this point) completer.complete(); // Verify no exception is thrown await tester.pumpAndSettle(); }); }); }