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
================================================

# Flutter ScrollView Observer
[](https://github.com/LinXunFeng/) [](https://pub.dev/packages/scrollview_observer) [](https://github.com/fluttercandies/flutter_scrollview_observer)
Language: 中文 | [English](https://github.com/fluttercandies/flutter_scrollview_observer)
这是一个可用于监听滚动视图中正在显示的子部件的组件库。
## ☕ 请我喝一杯咖啡
[](https://ko-fi.com/T6T4JKVRP) [](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
================================================

# Flutter ScrollView Observer
[](https://github.com/LinXunFeng/) [](https://pub.dev/packages/scrollview_observer) [](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
[](https://ko-fi.com/T6T4JKVRP) [](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