├── example ├── README.md ├── ios │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── .gitignore ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ ├── manifest.json │ └── index.html ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── lib │ ├── widgets.dart │ ├── route_path.dart │ ├── mock_data.dart │ ├── example_listview.dart │ ├── example_sliver.dart │ ├── example_side_header.dart │ ├── example_animatable_header.dart │ ├── example_custom_section.dart │ ├── main.dart │ ├── example_scroll_to_index.dart │ ├── example_custom_section_animation.dart │ ├── example_nested_listview.dart │ ├── example_pull_to_refresh.dart │ └── example_nested_scroll_view.dart ├── .gitignore ├── analysis_options.yaml ├── integration_test │ └── example_tests.dart └── pubspec.yaml ├── doc └── images │ └── sliverlist.gif ├── publish.txt ├── lib ├── sticky_and_expandable_list.dart └── src │ ├── expandable_list_view.dart │ ├── sliver_expandable_list.dart │ └── expandable_section_container.dart ├── .metadata ├── .gitignore ├── .github └── FUNDING.yml ├── LICENSE ├── CHANGELOG.md ├── pubspec.yaml ├── README_zh_CN.md └── README.md /example/README.md: -------------------------------------------------------------------------------- 1 | ## example -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/web/favicon.png -------------------------------------------------------------------------------- /doc/images/sliverlist.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/doc/images/sliverlist.gif -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /publish.txt: -------------------------------------------------------------------------------- 1 | # 正式发布,国内用户应该都有使用flutter提供的中国镜像,所以上传时要指明上传到https://pub.dartlang.org地址。 2 | flutter packages pub publish --server=https://pub.dartlang.org 3 | -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/sticky_and_expandable_list.dart: -------------------------------------------------------------------------------- 1 | library expandable_list; 2 | 3 | export 'src/expandable_section_container.dart'; 4 | export 'src/expandable_list_view.dart'; 5 | export 'src/sliver_expandable_list.dart'; 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 27321ebbad34b0a3fafe99fac037102196d655ff 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 097d3313d8e2c7f901932d63e537c1acefb87800 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /example/lib/widgets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomAppBar extends AppBar { 4 | CustomAppBar({ 5 | Key? key, 6 | String title = "", 7 | }) : super( 8 | key: key, 9 | title: Text( 10 | title, 11 | style: TextStyle(color: Colors.white), 12 | ), 13 | iconTheme: IconThemeData(color: Colors.white), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/tools/private-files.html 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | .pub/ 7 | build/ 8 | # If you're building an application, you may want to check-in your pubspec.lock 9 | pubspec.lock 10 | 11 | # Directory created by dartdoc 12 | # If you don't generate documentation locally you can remove this line. 13 | doc/api/ 14 | 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | .DS_Store 20 | .vscode/ 21 | .history/ 22 | .last_build_id 23 | flutter_*.log 24 | Podfile.lock -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/lib/route_path.dart: -------------------------------------------------------------------------------- 1 | class RoutePath { 2 | static const home = "/"; 3 | static const listView = "/listView"; 4 | static const sliver = "/sliver"; 5 | static const animatableHeader = "/animatableHeader"; 6 | static const customSection = "/customSection"; 7 | static const nestedListView = "/subListView"; 8 | static const customSectionAnimation = "/customSectionAnimation"; 9 | static const nestedScrollView = "/nestedScrollView"; 10 | static const sideHeader = "/sideHeader"; 11 | static const scrollToIndex = "/scrollToIndex"; 12 | static const pullToRefresh = "/pullToRefresh"; 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://www.paypal.com/paypalme/tp7309','https://user-images.githubusercontent.com/5046191/118354036-b075ca80-b59b-11eb-862e-ffd1b8e1659f.png'] 13 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Romain Rastel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.3 2 | - remove unused log. 3 | - 4 | ## 1.1.2 5 | - [BugFix]sticky header not work when section's item is less.#45 6 | 7 | ## 1.1.0 8 | - migrate to flutter 3.0.0. 9 | 10 | ## 1.0.3 11 | - [Bugfix]Adding a separatorBuilder to the package's ListView example causes overlapping items #37 12 | 13 | ## 1.0.1 14 | - fix example warning. 15 | 16 | ## 1.0.0 17 | - update example to nullsafety version. 18 | 19 | ## 1.0.0-nullsafety.1 20 | - [Bugfix]compatibility error: type 'List' is not a subtype of type 'List' in type cast #28. 21 | 22 | ## 1.0.0-nullsafety 23 | - migrate to nullsafety version. 24 | 25 | ## 0.3.1 26 | - support add nested list view, but has some limit. 27 | - add integration_test for example. 28 | 29 | ## 0.3.0 30 | - support overlapsContent property. 31 | - add more examples. 32 | - [Bugfix]show error when collapse sticky section or last section. 33 | 34 | ## 0.2.3 35 | - [Bugfix]the header will move from top to the position where I click last item/position #18 36 | 37 | ## 0.2.1 38 | - fix bugs on Flutter 1.17.0. 39 | 40 | ## 0.2.0 41 | - import widgets.dart instead of material.dart 42 | - fix some bugs. 43 | 44 | ## 0.2.0-beta 45 | - Use more friendly api. 46 | - Support customize expand/collapse animation. 47 | - fix some bugs. 48 | 49 | ## 0.1.0 50 | 51 | - Initial repository. -------------------------------------------------------------------------------- /example/lib/mock_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 2 | 3 | /// 4 | /// create some example data. 5 | /// 6 | class MockData { 7 | ///return a example list, by default, we have 4 sections, 8 | ///each section has 5 items. 9 | static List getExampleSections( 10 | [int sectionSize = 10, int itemSize = 5]) { 11 | var sections = List.empty(growable: true); 12 | for (int i = 0; i < sectionSize; i++) { 13 | var section = ExampleSection() 14 | ..header = "Header#$i" 15 | ..items = List.generate(itemSize, (index) => "ListTile #$index") 16 | ..expanded = true; 17 | sections.add(section); 18 | } 19 | return sections; 20 | } 21 | } 22 | 23 | ///Section model example 24 | /// 25 | ///Section model must implements ExpandableListSection, each section has 26 | ///expand state, sublist. "T" is the model of each item in the sublist. 27 | class ExampleSection implements ExpandableListSection { 28 | //store expand state. 29 | late bool expanded; 30 | 31 | //return item model list. 32 | late List items; 33 | 34 | //example header, optional 35 | late String header; 36 | 37 | @override 38 | List getItems() { 39 | return items; 40 | } 41 | 42 | @override 43 | bool isSectionExpanded() { 44 | return expanded; 45 | } 46 | 47 | @override 48 | void setSectionExpanded(bool expanded) { 49 | this.expanded = expanded; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/expandable_list_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | import '../sticky_and_expandable_list.dart'; 5 | 6 | /// A scrollable list of widgets arranged linearly, support expand/collapse item and 7 | /// sticky header. 8 | /// all build options are set in [SliverExpandableChildDelegate], this is to avoid 9 | /// [SliverExpandableList] use generics. 10 | class ExpandableListView extends BoxScrollView { 11 | ///same as ListView 12 | final SliverExpandableChildDelegate builder; 13 | 14 | ExpandableListView({ 15 | Key? key, 16 | required this.builder, 17 | bool reverse = false, 18 | ScrollController? controller, 19 | bool? primary, 20 | ScrollPhysics? physics, 21 | bool shrinkWrap = false, 22 | EdgeInsetsGeometry? padding, 23 | double? cacheExtent, 24 | DragStartBehavior dragStartBehavior = DragStartBehavior.start, 25 | }) : super( 26 | key: key, 27 | scrollDirection: Axis.vertical, 28 | reverse: reverse, 29 | controller: controller, 30 | primary: primary, 31 | physics: physics, 32 | shrinkWrap: shrinkWrap, 33 | padding: padding, 34 | cacheExtent: cacheExtent, 35 | semanticChildCount: builder.sectionList.length, 36 | dragStartBehavior: dragStartBehavior, 37 | ); 38 | 39 | @override 40 | Widget buildChildLayout(BuildContext context) { 41 | return SliverExpandableList( 42 | builder: builder, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | #include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: sticky_and_expandable_list 2 | description: Build a grouped list, which support expand/collapse section and sticky header, support use it with sliver widget. 3 | version: 1.1.3 4 | homepage: https://github.com/tp7309/flutter_sticky_and_expandable_list 5 | issue_tracker: https://github.com/tp7309/flutter_sticky_and_expandable_list/issues 6 | 7 | environment: 8 | sdk: '>=2.12.0 <3.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | # For information on the generic Dart part of this file, see the 19 | # following page: https://dart.dev/tools/pub/pubspec 20 | 21 | # The following section is specific to Flutter. 22 | flutter: 23 | 24 | # To add assets to your package, add an assets section, like this: 25 | # assets: 26 | # - images/a_dot_burr.jpeg 27 | # - images/a_dot_ham.jpeg 28 | # 29 | # For details regarding assets in packages, see 30 | # https://flutter.dev/assets-and-images/#from-packages 31 | # 32 | # An image asset can refer to one or more resolution-specific "variants", see 33 | # https://flutter.dev/assets-and-images/#resolution-aware. 34 | 35 | # To add custom fonts to your package, add a fonts section here, 36 | # in this "flutter" section. Each entry in this list should have a 37 | # "family" key with the font family name, and a "fonts" key with a 38 | # list giving the asset and other descriptors for the font. For 39 | # example: 40 | # fonts: 41 | # - family: Schyler 42 | # fonts: 43 | # - asset: fonts/Schyler-Regular.ttf 44 | # - asset: fonts/Schyler-Italic.ttf 45 | # style: italic 46 | # - family: Trajan Pro 47 | # fonts: 48 | # - asset: fonts/TrajanPro.ttf 49 | # - asset: fonts/TrajanPro_Bold.ttf 50 | # weight: 700 51 | # 52 | # For details regarding fonts in packages, see 53 | # https://flutter.dev/custom-fonts/#from-packages 54 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "com.example.example" 47 | minSdkVersion flutter.minSdkVersion 48 | targetSdkVersion flutter.targetSdkVersion 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies { 67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 68 | } 69 | -------------------------------------------------------------------------------- /example/lib/example_listview.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/mock_data.dart'; 2 | import 'package:example/widgets.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 5 | 6 | class ExampleListView extends StatefulWidget { 7 | @override 8 | _ExampleListViewState createState() => _ExampleListViewState(); 9 | } 10 | 11 | class _ExampleListViewState extends State { 12 | var sectionList = MockData.getExampleSections(); 13 | 14 | @override 15 | void initState() { 16 | super.initState(); 17 | } 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | //In this example, we create a custom model class(ExampleSection). 22 | //class ExampleSection implements ExpandableListSection {} 23 | //so: SliverExpandableChildDelegate() 24 | return Scaffold( 25 | appBar: CustomAppBar(title: "ListView Example"), 26 | body: ExpandableListView( 27 | builder: SliverExpandableChildDelegate( 28 | sectionList: sectionList, 29 | headerBuilder: _buildHeader, 30 | itemBuilder: (context, sectionIndex, itemIndex, index) { 31 | String item = sectionList[sectionIndex].items[itemIndex]; 32 | return ListTile( 33 | leading: CircleAvatar( 34 | child: Text("$index"), 35 | ), 36 | title: Text(item), 37 | ); 38 | }), 39 | )); 40 | } 41 | 42 | Widget _buildHeader(BuildContext context, int sectionIndex, int index) { 43 | ExampleSection section = sectionList[sectionIndex]; 44 | return InkWell( 45 | child: Container( 46 | color: Colors.lightBlue, 47 | height: 48, 48 | padding: EdgeInsets.only(left: 20), 49 | alignment: Alignment.centerLeft, 50 | child: Text( 51 | section.header, 52 | style: TextStyle(color: Colors.white), 53 | )), 54 | onTap: () { 55 | //toggle section expand state 56 | setState(() { 57 | section.setSectionExpanded(!section.isSectionExpanded()); 58 | }); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /example/integration_test/example_tests.dart: -------------------------------------------------------------------------------- 1 | // import 'package:example/route_path.dart'; 2 | // import 'package:flutter/material.dart'; 3 | // import 'package:flutter/widgets.dart'; 4 | // import 'package:flutter_test/flutter_test.dart'; 5 | // import 'package:integration_test/integration_test.dart'; 6 | // import 'package:example/main.dart' as app; 7 | // import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 8 | // 9 | // void main() { 10 | // IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 11 | // 12 | // group("example_listivew_test", () { 13 | // testWidgets("sticky", (WidgetTester tester) async { 14 | // app.main(); 15 | // await tester.pumpAndSettle(); 16 | // await tester.tap(find.byKey(ValueKey(RoutePath.listView))); 17 | // 18 | // await tester.pumpAndSettle(); 19 | // var listView = find.byType(ExpandableListView); 20 | // var header0 = find.text("Header#0"); 21 | // var header1 = find.text("Header#1"); 22 | // var header2 = find.text("Header#2"); 23 | // 24 | // //initial position. 25 | // expect(header1, findsOneWidget); 26 | // expect(header2, findsOneWidget); 27 | // 28 | // //Header#1 sticky 29 | // await scrollTo(tester, listView, Offset(0, -200)); 30 | // expect(header1, findsOneWidget); 31 | // expect(header2, findsOneWidget); 32 | // await tester.pumpAndSettle(Duration(seconds: 1)); 33 | // 34 | // // //Header#2 sticky 35 | // await scrollTo(tester, listView, Offset(0, -300)); 36 | // expect(header0, findsNothing); 37 | // expect(header1, findsNothing); 38 | // expect(header2, findsOneWidget); 39 | // await tester.pumpAndSettle(Duration(seconds: 30)); 40 | // }); 41 | // }); 42 | // } 43 | // 44 | // Future scrollTo(WidgetTester tester, Finder finder, Offset offset) async { 45 | // await tester.timedDrag(finder, offset, Duration(milliseconds: 300)); 46 | // } 47 | // 48 | // // Future scrollTo(WidgetTester tester, Finder finder, String text, 49 | // // {Type widgetType = CircleAvatar, Offset offset = Offset.zero}) async { 50 | // // var widget = find.widgetWithText(widgetType, text); 51 | // // var finderOffset = tester.getTopLeft(finder); 52 | // // var itemOffset = tester.getTopLeft(widget); 53 | // // var finalOffset = Offset(finderOffset.dx - itemOffset.dx + offset.dx, 54 | // // finderOffset.dy - itemOffset.dy + offset.dy); 55 | // // await tester.timedDrag(finder, finalOffset, Duration(milliseconds: 300)); 56 | // // } 57 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/lib/example_sliver.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 3 | 4 | import 'mock_data.dart'; 5 | 6 | class ExampleSliver extends StatefulWidget { 7 | @override 8 | _ExampleSliverState createState() => _ExampleSliverState(); 9 | } 10 | 11 | class _ExampleSliverState extends State { 12 | var sectionList = MockData.getExampleSections(); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return SafeArea( 17 | child: Scaffold( 18 | body: CustomScrollView( 19 | slivers: [ 20 | SliverAppBar( 21 | pinned: true, 22 | floating: true, 23 | expandedHeight: 200, 24 | flexibleSpace: FlexibleSpaceBar( 25 | title: Text( 26 | "Sliver Example", 27 | style: TextStyle(color: Colors.white), 28 | ), 29 | ), 30 | iconTheme: IconThemeData(color: Colors.white), 31 | ), 32 | SliverExpandableList( 33 | builder: SliverExpandableChildDelegate( 34 | sectionList: sectionList, 35 | headerBuilder: _buildHeader, 36 | itemBuilder: (context, sectionIndex, itemIndex, index) { 37 | String item = sectionList[sectionIndex].items[itemIndex]; 38 | return ListTile( 39 | leading: CircleAvatar( 40 | child: Text("$index"), 41 | ), 42 | title: Text(item), 43 | ); 44 | }, 45 | ), 46 | ), 47 | ], 48 | ), 49 | ), 50 | ); 51 | } 52 | 53 | Widget _buildHeader(BuildContext context, int sectionIndex, int index) { 54 | ExampleSection section = sectionList[sectionIndex]; 55 | return InkWell( 56 | child: Container( 57 | color: Colors.lightBlue, 58 | height: 48, 59 | padding: EdgeInsets.only(left: 20), 60 | alignment: Alignment.centerLeft, 61 | child: Text( 62 | section.header, 63 | style: TextStyle(color: Colors.white), 64 | )), 65 | onTap: () { 66 | //toggle section expand state 67 | setState(() { 68 | section.setSectionExpanded(!section.isSectionExpanded()); 69 | }); 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /example/lib/example_side_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/mock_data.dart'; 2 | import 'package:example/widgets.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 5 | 6 | class ExampleSideHeader extends StatefulWidget { 7 | @override 8 | _ExampleSideHeaderState createState() => _ExampleSideHeaderState(); 9 | } 10 | 11 | class _ExampleSideHeaderState extends State { 12 | var sectionList = MockData.getExampleSections(); 13 | 14 | var _controller = ExpandableListController(); 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | } 20 | 21 | @override 22 | void dispose() { 23 | _controller.dispose(); 24 | super.dispose(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Scaffold( 30 | appBar: CustomAppBar(title: "Side Header Example"), 31 | body: ExpandableListView( 32 | builder: SliverExpandableChildDelegate( 33 | overlapsContent: true, 34 | controller: _controller, 35 | sectionList: sectionList, 36 | headerBuilder: _buildHeader, 37 | itemBuilder: (context, sectionIndex, itemIndex, index) { 38 | String item = sectionList[sectionIndex].items[itemIndex]; 39 | return Padding( 40 | padding: const EdgeInsets.only(left: 50), 41 | child: Container( 42 | color: Colors.black26, 43 | child: ListTile( 44 | title: Text( 45 | item, 46 | style: TextStyle(color: Colors.white), 47 | ), 48 | ), 49 | ), 50 | ); 51 | }, 52 | separatorBuilder: (context, isSectionSeparator, index) { 53 | return isSectionSeparator 54 | ? SizedBox( 55 | height: 15, 56 | ) 57 | : Container(); 58 | }), 59 | )); 60 | } 61 | 62 | Widget _buildHeader(BuildContext context, int sectionIndex, int index) { 63 | return ExpandableAutoLayoutWidget( 64 | trigger: ExpandableDefaultAutoLayoutTrigger(_controller), 65 | builder: (context) { 66 | double opacity = _controller.switchingSectionIndex == sectionIndex 67 | ? (1 - _controller.percent) 68 | : 1; 69 | return Container( 70 | padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4), 71 | child: CircleAvatar( 72 | backgroundColor: Colors.lightBlue.withOpacity(opacity), 73 | child: Text( 74 | "$sectionIndex", 75 | style: TextStyle(color: Colors.white), 76 | ), 77 | )); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: flutter_sticky_and_expandable_list example project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | publish_to: none 16 | 17 | environment: 18 | sdk: '>=2.12.0 <3.0.0' 19 | 20 | dependencies: 21 | flutter: 22 | sdk: flutter 23 | 24 | # The following adds the Cupertino Icons font to your application. 25 | # Use with the CupertinoIcons class for iOS style icons. 26 | # cupertino_icons: ^0.1.2 27 | sticky_and_expandable_list: 28 | path: ../ 29 | scroll_to_index: ^3.0.0 30 | pull_to_refresh: ^2.0.0 31 | 32 | dev_dependencies: 33 | integration_test: 34 | sdk: flutter 35 | flutter_test: 36 | sdk: flutter 37 | flutter_driver: 38 | sdk: flutter 39 | 40 | # For information on the generic Dart part of this file, see the 41 | # following page: https://dart.dev/tools/pub/pubspec 42 | 43 | # The following section is specific to Flutter. 44 | flutter: 45 | 46 | # The following line ensures that the Material Icons font is 47 | # included with your application, so that you can use the icons in 48 | # the material Icons class. 49 | uses-material-design: true 50 | 51 | # To add assets to your application, add an assets section, like this: 52 | # assets: 53 | # - images/a_dot_burr.jpeg 54 | # - images/a_dot_ham.jpeg 55 | 56 | # An image asset can refer to one or more resolution-specific "variants", see 57 | # https://flutter.dev/assets-and-images/#resolution-aware. 58 | 59 | # For details regarding adding assets from package dependencies, see 60 | # https://flutter.dev/assets-and-images/#from-packages 61 | 62 | # To add custom fonts to your application, add a fonts section here, 63 | # in this "flutter" section. Each entry in this list should have a 64 | # "family" key with the font family name, and a "fonts" key with a 65 | # list giving the asset and other descriptors for the font. For 66 | # example: 67 | # fonts: 68 | # - family: Schyler 69 | # fonts: 70 | # - asset: fonts/Schyler-Regular.ttf 71 | # - asset: fonts/Schyler-Italic.ttf 72 | # style: italic 73 | # - family: Trajan Pro 74 | # fonts: 75 | # - asset: fonts/TrajanPro.ttf 76 | # - asset: fonts/TrajanPro_Bold.ttf 77 | # weight: 700 78 | # 79 | # For details regarding fonts from package dependencies, 80 | # see https://flutter.dev/custom-fonts/#from-packages 81 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/lib/example_animatable_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/mock_data.dart'; 2 | import 'package:example/widgets.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 5 | 6 | class ExampleAnimatableHeader extends StatefulWidget { 7 | @override 8 | _ExampleAnimatableHeaderState createState() => 9 | _ExampleAnimatableHeaderState(); 10 | } 11 | 12 | class _ExampleAnimatableHeaderState extends State { 13 | var sectionList = MockData.getExampleSections(); 14 | var _controller = ExpandableListController(); 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | } 20 | 21 | @override 22 | void dispose() { 23 | _controller.dispose(); 24 | super.dispose(); 25 | } 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Scaffold( 30 | appBar: CustomAppBar(title: "Animatable Header Example"), 31 | body: CustomScrollView( 32 | slivers: [ 33 | SliverToBoxAdapter( 34 | child: Container( 35 | height: 100, 36 | width: 100, 37 | color: Colors.white, 38 | alignment: Alignment.center, 39 | child: Text( 40 | "PlaceHolder", 41 | style: TextStyle(color: Colors.black), 42 | ), 43 | )), 44 | SliverExpandableList( 45 | builder: SliverExpandableChildDelegate( 46 | sectionList: sectionList, 47 | headerBuilder: _buildHeader, 48 | controller: _controller, 49 | itemBuilder: (context, sectionIndex, itemIndex, index) { 50 | String item = sectionList[sectionIndex].items[itemIndex]; 51 | return ListTile( 52 | leading: CircleAvatar( 53 | child: Text("$index"), 54 | ), 55 | title: Text(item), 56 | ); 57 | }), 58 | ) 59 | ], 60 | ), 61 | ); 62 | } 63 | 64 | Widget _buildHeader(BuildContext context, int sectionIndex, int index) { 65 | var section = sectionList[sectionIndex]; 66 | return ExpandableAutoLayoutWidget( 67 | trigger: ExpandableDefaultAutoLayoutTrigger(_controller), 68 | builder: (context) { 69 | double opacity = _controller.switchingSectionIndex == sectionIndex 70 | ? (1 - _controller.percent) 71 | : 1; 72 | String headerText = section.header; 73 | if (_controller.switchingSectionIndex == sectionIndex) { 74 | headerText += " Switching"; 75 | } else if (_controller.stickySectionIndex == sectionIndex) { 76 | headerText += " Pinned"; 77 | } 78 | return Container( 79 | color: Colors.lightBlue.withOpacity(opacity), 80 | height: 48, 81 | padding: EdgeInsets.only(left: 20), 82 | alignment: Alignment.centerLeft, 83 | child: Text( 84 | headerText, 85 | style: TextStyle(color: Colors.white), 86 | )); 87 | }, 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /example/lib/example_custom_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/mock_data.dart'; 2 | import 'package:example/widgets.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 5 | 6 | class ExampleCustomSection extends StatefulWidget { 7 | @override 8 | _ExampleCustomSectionState createState() => _ExampleCustomSectionState(); 9 | } 10 | 11 | class _ExampleCustomSectionState extends State { 12 | var sectionList = MockData.getExampleSections(); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: CustomAppBar(title: "CustomSection Example"), 18 | body: ExpandableListView( 19 | builder: SliverExpandableChildDelegate( 20 | sectionList: sectionList, 21 | sectionBuilder: _buildSection, 22 | itemBuilder: (context, sectionIndex, itemIndex, index) { 23 | String item = sectionList[sectionIndex].items[itemIndex]; 24 | return SizedBox( 25 | child: ListTile( 26 | leading: CircleAvatar( 27 | child: Text("$index"), 28 | ), 29 | title: Text(item), 30 | ), 31 | ); 32 | }), 33 | )); 34 | } 35 | 36 | Widget _buildSection( 37 | BuildContext context, ExpandableSectionContainerInfo containerInfo) { 38 | containerInfo 39 | ..header = _buildHeader(context, containerInfo) 40 | ..content = _buildContent(context, containerInfo); 41 | return ExpandableSectionContainer( 42 | info: containerInfo, 43 | ); 44 | } 45 | 46 | Widget _buildHeader( 47 | BuildContext context, ExpandableSectionContainerInfo containerInfo) { 48 | ExampleSection section = sectionList[containerInfo.sectionIndex]; 49 | return InkWell( 50 | child: Container( 51 | color: Colors.lightBlue, 52 | height: 48, 53 | padding: EdgeInsets.only(left: 20), 54 | alignment: Alignment.centerLeft, 55 | child: Text( 56 | section.header, 57 | style: TextStyle(color: Colors.white), 58 | )), 59 | onTap: () { 60 | //toggle section expand state 61 | setState(() { 62 | section.setSectionExpanded(!section.isSectionExpanded()); 63 | }); 64 | }); 65 | } 66 | 67 | Widget _buildContent( 68 | BuildContext context, ExpandableSectionContainerInfo containerInfo) { 69 | ExampleSection section = sectionList[containerInfo.sectionIndex]; 70 | if (!section.isSectionExpanded()) { 71 | return Container(); 72 | } 73 | return GridView.builder( 74 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 75 | crossAxisCount: 2, 76 | childAspectRatio: 3, 77 | ), 78 | shrinkWrap: true, 79 | physics: NeverScrollableScrollPhysics(), 80 | itemBuilder: containerInfo.childDelegate!.builder as Widget Function( 81 | BuildContext, int), 82 | itemCount: containerInfo.childDelegate!.childCount, 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:example/example_custom_section_animation.dart'; 4 | import 'package:example/example_nested_scroll_view.dart'; 5 | import 'package:example/example_pull_to_refresh.dart'; 6 | import 'package:example/example_scroll_to_index.dart'; 7 | import 'package:example/example_side_header.dart'; 8 | import 'package:example/route_path.dart'; 9 | import 'package:example/widgets.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter/services.dart'; 12 | 13 | import 'example_animatable_header.dart'; 14 | import 'example_custom_section.dart'; 15 | import 'example_listview.dart'; 16 | import 'example_sliver.dart'; 17 | import 'example_nested_listview.dart'; 18 | 19 | void main() { 20 | runApp(MyApp()); 21 | if (Platform.isAndroid) { 22 | //set statusBar color 23 | var overlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.blue); 24 | SystemChrome.setSystemUIOverlayStyle(overlayStyle); 25 | } 26 | } 27 | 28 | class MyApp extends StatelessWidget { 29 | // This widget is the root of your application. 30 | @override 31 | Widget build(BuildContext context) { 32 | return MaterialApp( 33 | title: 'Flutter sticky and expandable list', 34 | theme: ThemeData( 35 | primarySwatch: Colors.lightBlue, 36 | ), 37 | routes: _buildRoutes(), 38 | ); 39 | } 40 | 41 | Map _buildRoutes() { 42 | return { 43 | RoutePath.home: (context) => _HomePage(), 44 | RoutePath.listView: (context) => ExampleListView(), 45 | RoutePath.sliver: (context) => ExampleSliver(), 46 | RoutePath.animatableHeader: (context) => ExampleAnimatableHeader(), 47 | RoutePath.customSection: (context) => ExampleCustomSection(), 48 | RoutePath.nestedListView: (context) => ExampleNestedListView(), 49 | RoutePath.customSectionAnimation: (context) => 50 | ExampleCustomSectionAnimation(), 51 | RoutePath.nestedScrollView: (context) => ExampleNestedScrollView(), 52 | RoutePath.sideHeader: (context) => ExampleSideHeader(), 53 | RoutePath.scrollToIndex: (context) => ExampleScrollToIndex(), 54 | RoutePath.pullToRefresh: (context) => ExamplePullToRefresh(), 55 | }; 56 | } 57 | } 58 | 59 | class _HomePage extends StatelessWidget { 60 | @override 61 | Widget build(BuildContext context) { 62 | return Scaffold( 63 | appBar: CustomAppBar(title: "Flutter sticky and expandable list"), 64 | body: ListView( 65 | children: [ 66 | _Item("ListView Example", RoutePath.listView), 67 | _Item("Sliver Example", RoutePath.sliver), 68 | _Item("Animatable Header Example", RoutePath.animatableHeader), 69 | _Item("CustomSection Example", RoutePath.customSection), 70 | _Item("NestedListView Example", RoutePath.nestedListView), 71 | _Item("CustomSectionAnimation Example", 72 | RoutePath.customSectionAnimation), 73 | _Item("NestedScrollView Example", RoutePath.nestedScrollView), 74 | _Item("Side Header Example", RoutePath.sideHeader), 75 | _Item("ScrollToIndex Example", RoutePath.scrollToIndex), 76 | _Item("PullToRefresh Example", RoutePath.pullToRefresh), 77 | ], 78 | ), 79 | ); 80 | } 81 | } 82 | 83 | class _Item extends StatelessWidget { 84 | final String jumpUrl; 85 | final String title; 86 | 87 | _Item(this.title, this.jumpUrl); 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | return Card( 92 | key: ValueKey(jumpUrl), 93 | color: Theme.of(context).primaryColor, 94 | child: TextButton( 95 | onPressed: () => Navigator.of(context).pushNamed(jumpUrl), 96 | child: Text( 97 | title, 98 | style: TextStyle(color: Colors.white, fontSize: 18), 99 | ), 100 | ), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README_zh_CN.md: -------------------------------------------------------------------------------- 1 | # sticky_and_expandable_list 2 | 3 | 可拆叠列表的 Flutter 实现,支持粘性头部,可以与 Sliver 家族控件配合使用。 4 | 5 | [![Pub](https://img.shields.io/pub/v/sticky_and_expandable_list.svg)](https://pub.dartlang.org/packages/sticky_and_expandable_list) 6 | 7 | ![Screenshot](https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/master/doc/images/sliverlist.gif) 8 | 9 | ## 特性 10 | 11 | - 支持构建可切换拆叠/展开状态的 ListView,支持粘性头部。 12 | - 可以与 Sliver 家族控件配合使用,用在如 CustomScrollView、NestedScrollView 中。 13 | - 通过 controller 监听当前粘性头部的滚动偏移量,当前被固定 header 的索引和正在隐藏/显示的 header 的索引值。 14 | - 整个列表是一个类似 ListView 的控件,Builder 方式创建分组项,所以支持大量数据显示,不会把所有分组全部创建出来。 15 | - 可使用 sectionBuilder 进行更多 section 控件定制,如背景、自定义折叠/展开动画、不同 section 的布局方式定制等。 16 | - 支持添加分隔线。 17 | - 支持标题覆盖内容。 18 | - 支持使用[scroll-to-index](https://github.com/quire-io/scroll-to-index)滚动列表到指定位置。 19 | - 支持使用[pull_to_refresh](https://github.com/peng8350/flutter_pulltorefresh)之类库进行下拉刷新与加载更多。 20 | 21 | 22 | ## 开始 23 | 24 | 在 Flutter 项目中的 `pubspec.yaml` 文件中添加如下依赖。 25 | 26 | ```yaml 27 | dependencies: 28 | sticky_and_expandable_list: ^1.1.3 29 | ``` 30 | 31 | ## 基础使用示例 32 | `sectionList`是`ExpandableListView`的数据源,需要使用者自己定义。 33 | 我们需要创建一个模型类列表去存储每个组的信息,这个模型类需要实现`ExpandableListSection`接口。 34 | ```dart 35 | //在这个示例中,创建了一个自定义的Section类(ExampleSection)。 36 | //class ExampleSection implements ExpandableListSection {} 37 | //so: SliverExpandableChildDelegate() 38 | List sectionList = List(); 39 | return ExpandableListView( 40 | builder: SliverExpandableChildDelegate( 41 | sectionList: sectionList, 42 | headerBuilder: (context, sectionIndex, index) => 43 | Text("Header #$sectionIndex"), 44 | itemBuilder: (context, sectionIndex, itemIndex, index) { 45 | String item = sectionList[sectionIndex].items[itemIndex]; 46 | return ListTile( 47 | leading: CircleAvatar( 48 | child: Text("$index"), 49 | ), 50 | title: Text(item), 51 | ); 52 | }), 53 | ); 54 | ``` 55 | 56 | [详细示例](https://github.com/tp7309/flutter_sticky_and_expandable_list/tree/master/example) 57 | 58 | 如果是与Sliver控件一起使用的话,使用**SliverExpandableList** 来代替ExpandableListView. 59 | 60 | ## 常见问题 61 | 62 | ### 如何切换列表的展开/拆叠状态? 63 | 64 | ```dart 65 | setState(() { 66 | sectionList[i].setSectionExpanded(true); 67 | }); 68 | ``` 69 | 70 | [Example](https://github.com/tp7309/flutter_sticky_and_expandable_list/blob/master/example/lib/example_listview.dart) 71 | 72 | ### 如何监听当前粘性头部的滚动偏移量?如何得知哪个 Header 是粘性头部? 73 | 74 | ```dart 75 | @override 76 | Widget build(BuildContext context) { 77 | ExpandableListView( 78 | builder: SliverExpandableChildDelegate( 79 | headerController: _getHeaderController(), 80 | ), 81 | ) 82 | } 83 | 84 | _getHeaderController() { 85 | var controller = ExpandableListController(); 86 | controller.addListener(() { 87 | print("switchingSectionIndex:${controller.switchingSectionIndex}, stickySectionIndex:" + 88 | "${controller.stickySectionIndex},scrollPercent:${controller.percent}"); 89 | }); 90 | return controller; 91 | } 92 | ``` 93 | 94 | ### 如何定制每一组数据的背景、阴影等信息? 95 | 96 | 使用[sectionBuilder](https://github.com/tp7309/flutter_sticky_and_expandable_list/blob/master/example/lib/example_custom_section_animation.dart) 97 | 返回自定义的 Widget. 98 | 99 | ### 自定义折叠动画 100 | 101 | 使用 Flutter 自带动画进行定制: 102 | [Example](https://github.com/tp7309/flutter_sticky_and_expandable_list/blob/master/example/lib/example_custom_section_animation.dart) 103 | 104 | ## 更新日志 105 | 106 | [CHANGELOG](https://github.com/tp7309/flutter_sticky_and_expandable_list/blob/master/CHANGELOG.md) 107 | 108 | ## 支持 109 | 110 | 觉得对有帮助,请作者喝杯咖啡 (微信): 111 | 112 | ![qrcode](https://user-images.githubusercontent.com/5046191/118354036-b075ca80-b59b-11eb-862e-ffd1b8e1659f.png) 113 | -------------------------------------------------------------------------------- /example/lib/example_scroll_to_index.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/mock_data.dart'; 2 | import 'package:example/widgets.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:scroll_to_index/scroll_to_index.dart'; 5 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 6 | 7 | class ExampleScrollToIndex extends StatefulWidget { 8 | @override 9 | _ExampleScrollToIndexState createState() => _ExampleScrollToIndexState(); 10 | } 11 | 12 | class _ExampleScrollToIndexState extends State { 13 | var sectionList = MockData.getExampleSections(10, 5); 14 | 15 | late AutoScrollController scrollController; 16 | int counter = 0; 17 | int maxCount = 10 * (5 + 1) - 1; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | scrollController = AutoScrollController( 23 | viewportBoundaryGetter: () => 24 | Rect.fromLTRB(0, 0, 0, MediaQuery.of(context).padding.bottom), 25 | axis: Axis.vertical); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | //In this example, we create a custom model class(ExampleSection). 31 | //class ExampleSection implements ExpandableListSection {} 32 | //so: SliverExpandableChildDelegate() 33 | return Scaffold( 34 | appBar: CustomAppBar(title: "ListView Example"), 35 | body: ExpandableListView( 36 | controller: scrollController, 37 | builder: SliverExpandableChildDelegate( 38 | sectionList: sectionList, 39 | headerBuilder: _buildHeader, 40 | itemBuilder: (context, sectionIndex, itemIndex, index) { 41 | String item = sectionList[sectionIndex].items[itemIndex]; 42 | return _wrapScrollTag( 43 | index: index, 44 | child: ListTile( 45 | leading: CircleAvatar( 46 | child: Text("$index"), 47 | ), 48 | title: Text(item), 49 | ), 50 | ); 51 | }), 52 | ), 53 | floatingActionButton: InkWell( 54 | onTap: _scrollToIndex, 55 | child: Container( 56 | height: 48, 57 | padding: EdgeInsets.all(10), 58 | alignment: Alignment.center, 59 | color: Colors.blueGrey, 60 | child: Text("Click Me: ${counter.toString()}", style: TextStyle(color: Colors.white),), 61 | ), 62 | ), 63 | ); 64 | } 65 | 66 | Widget _buildHeader(BuildContext context, int sectionIndex, int index) { 67 | ExampleSection section = sectionList[sectionIndex]; 68 | return _wrapScrollTag( 69 | index: index, 70 | child: InkWell( 71 | child: Container( 72 | color: Colors.lightBlue, 73 | height: 48, 74 | padding: EdgeInsets.only(left: 20), 75 | alignment: Alignment.centerLeft, 76 | child: Text( 77 | section.header, 78 | style: TextStyle(color: Colors.white), 79 | )), 80 | onTap: () { 81 | //toggle section expand state 82 | setState(() { 83 | section.setSectionExpanded(!section.isSectionExpanded()); 84 | }); 85 | }), 86 | ); 87 | } 88 | 89 | Future _scrollToIndex() async { 90 | setState(() { 91 | counter++; 92 | if (counter >= maxCount) counter = 0; 93 | }); 94 | 95 | await scrollController.scrollToIndex(counter, 96 | preferPosition: AutoScrollPosition.begin); 97 | scrollController.highlight(counter); 98 | } 99 | 100 | Widget _wrapScrollTag({required int index, required Widget child}) => 101 | AutoScrollTag( 102 | key: ValueKey(index), 103 | controller: scrollController, 104 | index: index, 105 | child: child, 106 | highlightColor: Colors.black.withOpacity(0.1), 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | example 33 | 34 | 35 | 36 | 44 | 47 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sticky_and_expandable_list 2 | Flutter implementation of sticky headers and expandable list.Support use it in a CustomScrollView. 3 | 4 | [![Pub](https://img.shields.io/pub/v/sticky_and_expandable_list.svg)](https://pub.dartlang.org/packages/sticky_and_expandable_list) 5 | README i18n:[中文说明](https://github.com/tp7309/flutter_sticky_and_expandable_list/blob/master/README_zh_CN.md) 6 | 7 | ![Screenshot](https://raw.githubusercontent.com/tp7309/flutter_sticky_and_expandable_list/master/doc/images/sliverlist.gif) 8 | 9 | ## Features 10 | 11 | - Build a grouped list, which support expand/collapse section and sticky header. 12 | - Use it with CustomScrollView、SliverAppBar. 13 | - Listen the scroll offset of current sticky header, current sticky header index and switching header index. 14 | - Only use one list widget, so it supports large data and a normal memory usage. 15 | - More section customization support, you can return a new section widget by sectionBuilder, to customize background,expand/collapse animation, section layout, and so on. 16 | - Support add divider. 17 | - Support overlap content. 18 | - Support scroll to index like ListView, by [scroll-to-index](https://github.com/quire-io/scroll-to-index). 19 | - Pull to refresh and load more, by [pull_to_refresh](https://github.com/peng8350/flutter_pulltorefresh). 20 | 21 | ## Getting Started 22 | 23 | In the `pubspec.yaml` of your flutter project, add the following dependency: 24 | 25 | ```yaml 26 | dependencies: 27 | sticky_and_expandable_list: ^1.1.3 28 | ``` 29 | 30 | ## Basic Usage 31 | `sectionList` is a custom data source for `ExpandableListView`. 32 | We should create a model list to store the information of each section, the model must implement `ExpandableListSection`. 33 | ```dart 34 | //In this example, we create a custom model class(ExampleSection). 35 | //class ExampleSection implements ExpandableListSection {} 36 | //so: SliverExpandableChildDelegate() 37 | List sectionList = List(); 38 | return ExpandableListView( 39 | builder: SliverExpandableChildDelegate( 40 | sectionList: sectionList, 41 | headerBuilder: (context, sectionIndex, index) => 42 | Text("Header #$sectionIndex"), 43 | itemBuilder: (context, sectionIndex, itemIndex, index) { 44 | String item = sectionList[sectionIndex].items[itemIndex]; 45 | return ListTile( 46 | leading: CircleAvatar( 47 | child: Text("$index"), 48 | ), 49 | title: Text(item), 50 | ); 51 | }), 52 | ); 53 | ``` 54 | 55 | [Detail Examples](https://github.com/tp7309/flutter_sticky_and_expandable_list/tree/master/example/lib) 56 | 57 | If you want to use it with sliver widget, use **SliverExpandableList** instead of ExpandableListView. 58 | 59 | ## FAQ 60 | 61 | ### How to expand/collapse item? 62 | 63 | ```dart 64 | setState(() { 65 | sectionList[i].setSectionExpanded(true); 66 | }); 67 | ``` 68 | 69 | [Example](https://github.com/tp7309/flutter_sticky_and_expandable_list/blob/master/example/lib/example_listview.dart) 70 | 71 | ### How to listen current sticky header or the sticky header scroll offset? 72 | 73 | ```dart 74 | @override 75 | Widget build(BuildContext context) { 76 | ExpandableListView( 77 | builder: SliverExpandableChildDelegate( 78 | headerController: _getHeaderController(), 79 | ), 80 | ) 81 | } 82 | 83 | _getHeaderController() { 84 | var controller = ExpandableListController(); 85 | controller.addListener(() { 86 | print("switchingSectionIndex:${controller.switchingSectionIndex}, stickySectionIndex:" + 87 | "${controller.stickySectionIndex},scrollPercent:${controller.percent}"); 88 | }); 89 | return controller; 90 | } 91 | ``` 92 | 93 | ### How to set background for each section? 94 | 95 | Use [sectionBuilder](https://github.com/tp7309/flutter_sticky_and_expandable_list/blob/master/example/lib/example_custom_section_animation.dart) 96 | 97 | ### Customize expand/collapse animation support? 98 | 99 | [Example](https://github.com/tp7309/flutter_sticky_and_expandable_list/blob/master/example/lib/example_custom_section_animation.dart) 100 | 101 | ## Change Log 102 | 103 | [CHANGELOG](https://github.com/tp7309/flutter_sticky_and_expandable_list/blob/master/CHANGELOG.md) 104 | 105 | ## Donate 106 | 107 | Buy a cup of coffee for me (Scan by wechat): 108 | 109 | ![qrcode](https://user-images.githubusercontent.com/5046191/118354036-b075ca80-b59b-11eb-862e-ffd1b8e1659f.png) 110 | 111 | 112 | -------------------------------------------------------------------------------- /example/lib/example_custom_section_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/widgets.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 4 | 5 | import 'mock_data.dart'; 6 | 7 | class ExampleCustomSectionAnimation extends StatefulWidget { 8 | @override 9 | _ExampleCustomSectionAnimationState createState() => 10 | _ExampleCustomSectionAnimationState(); 11 | } 12 | 13 | class _ExampleCustomSectionAnimationState 14 | extends State { 15 | var sectionList = MockData.getExampleSections(3, 3); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Scaffold( 20 | appBar: CustomAppBar(title: "CustomSectionAnimation Example"), 21 | body: ExpandableListView( 22 | builder: SliverExpandableChildDelegate( 23 | sectionList: sectionList, 24 | itemBuilder: (context, sectionIndex, itemIndex, index) { 25 | String item = sectionList[sectionIndex].items[itemIndex]; 26 | return ListTile( 27 | leading: CircleAvatar( 28 | child: Text("$index"), 29 | ), 30 | title: Text(item), 31 | ); 32 | }, 33 | sectionBuilder: (context, containerInfo) => _SectionWidget( 34 | section: sectionList[containerInfo.sectionIndex], 35 | containerInfo: containerInfo, 36 | onStateChanged: () { 37 | //notify ExpandableListView that expand state has changed. 38 | WidgetsBinding.instance.addPostFrameCallback((_) { 39 | if (mounted) { 40 | setState(() {}); 41 | } 42 | }); 43 | }, 44 | ), 45 | ), 46 | )); 47 | } 48 | } 49 | 50 | class _SectionWidget extends StatefulWidget { 51 | final ExampleSection section; 52 | final ExpandableSectionContainerInfo containerInfo; 53 | final VoidCallback onStateChanged; 54 | 55 | _SectionWidget( 56 | {required this.section, 57 | required this.containerInfo, 58 | required this.onStateChanged}); 59 | 60 | @override 61 | __SectionWidgetState createState() => __SectionWidgetState(); 62 | } 63 | 64 | class __SectionWidgetState extends State<_SectionWidget> 65 | with SingleTickerProviderStateMixin { 66 | static final Animatable _halfTween = 67 | Tween(begin: 0.0, end: 0.5); 68 | late AnimationController _controller; 69 | 70 | late Animation _iconTurns; 71 | 72 | late Animation _heightFactor; 73 | 74 | @override 75 | void initState() { 76 | super.initState(); 77 | _controller = 78 | AnimationController(vsync: this, duration: Duration(milliseconds: 300)); 79 | _iconTurns = 80 | _controller.drive(_halfTween.chain(CurveTween(curve: Curves.easeIn))); 81 | _heightFactor = _controller.drive(CurveTween(curve: Curves.easeIn)); 82 | 83 | if (widget.section.isSectionExpanded()) { 84 | _controller.value = 1; 85 | } 86 | } 87 | 88 | @override 89 | void dispose() { 90 | _controller.dispose(); 91 | super.dispose(); 92 | } 93 | 94 | @override 95 | Widget build(BuildContext context) { 96 | widget.containerInfo 97 | ..header = _buildHeader(context) 98 | ..content = _buildContent(context); 99 | return ExpandableSectionContainer( 100 | info: widget.containerInfo, 101 | ); 102 | } 103 | 104 | Widget _buildHeader(BuildContext context) { 105 | return Container( 106 | color: Colors.lightBlue, 107 | child: ListTile( 108 | title: Text( 109 | widget.section.header, 110 | style: TextStyle(color: Colors.white), 111 | ), 112 | trailing: RotationTransition( 113 | turns: _iconTurns as Animation, 114 | child: const Icon( 115 | Icons.expand_more, 116 | color: Colors.white70, 117 | ), 118 | ), 119 | onTap: _onTap, 120 | ), 121 | ); 122 | } 123 | 124 | void _onTap() { 125 | widget.section.setSectionExpanded(!widget.section.isSectionExpanded()); 126 | if (widget.section.isSectionExpanded()) { 127 | widget.onStateChanged(); 128 | _controller.forward(); 129 | } else { 130 | _controller.reverse().then((_) { 131 | widget.onStateChanged(); 132 | }); 133 | } 134 | } 135 | 136 | Widget _buildContent(BuildContext context) { 137 | return SizeTransition( 138 | sizeFactor: _heightFactor, 139 | child: SliverExpandableChildDelegate.buildDefaultContent( 140 | context, widget.containerInfo), 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /example/lib/example_nested_listview.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/mock_data.dart'; 2 | import 'package:example/widgets.dart'; 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 6 | import 'package:example/example_custom_section.dart'; 7 | 8 | ///if you want user ListView inside ExpandableListView, you have two options: 9 | /// 10 | ///Option 1: 11 | ///use shrinkWrap:true, like [ExampleCustomSection] 12 | /// 13 | ///Option 2: 14 | ///wrap ListView with SizeBox/Container, like [ExampleNestedListView], fixed length. 15 | /// 16 | class ExampleNestedListView extends StatefulWidget { 17 | @override 18 | _ExampleNestedListViewState createState() => _ExampleNestedListViewState(); 19 | } 20 | 21 | class _ExampleNestedListViewState extends State { 22 | var sectionList = MockData.getExampleSections(10, 20); 23 | 24 | late ScrollController _scrollListener; 25 | 26 | Drag? drag; 27 | 28 | DragStartDetails? dragStartDetails; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _scrollListener = new ScrollController(); 34 | } 35 | 36 | @override 37 | void dispose() { 38 | _scrollListener.dispose(); 39 | super.dispose(); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return Scaffold( 45 | appBar: CustomAppBar(title: "NestedListView Example"), 46 | body: ExpandableListView( 47 | controller: _scrollListener, 48 | builder: SliverExpandableChildDelegate( 49 | sectionList: sectionList, 50 | sectionBuilder: _buildSection, 51 | itemBuilder: (context, sectionIndex, itemIndex, index) { 52 | String item = sectionList[sectionIndex].items[itemIndex]; 53 | return Container( 54 | color: Colors.white12, 55 | child: ListTile( 56 | leading: CircleAvatar( 57 | child: Text("$index"), 58 | ), 59 | title: Text(item), 60 | ), 61 | ); 62 | }), 63 | )); 64 | } 65 | 66 | Widget _buildSection( 67 | BuildContext context, ExpandableSectionContainerInfo containerInfo) { 68 | containerInfo 69 | ..header = _buildHeader(containerInfo) 70 | ..content = _buildContent(context, containerInfo); 71 | return ExpandableSectionContainer( 72 | info: containerInfo, 73 | ); 74 | } 75 | 76 | Widget _buildHeader(ExpandableSectionContainerInfo containerInfo) { 77 | ExampleSection section = sectionList[containerInfo.sectionIndex]; 78 | return InkWell( 79 | child: Container( 80 | color: Colors.lightBlue, 81 | height: 48, 82 | padding: EdgeInsets.only(left: 20), 83 | alignment: Alignment.centerLeft, 84 | child: Text( 85 | section.header, 86 | style: TextStyle(color: Colors.white), 87 | )), 88 | onTap: () { 89 | //toggle section expand state 90 | setState(() { 91 | section.setSectionExpanded(!section.isSectionExpanded()); 92 | }); 93 | }); 94 | } 95 | 96 | Widget _buildContent( 97 | BuildContext context, ExpandableSectionContainerInfo containerInfo) { 98 | ExampleSection section = sectionList[containerInfo.sectionIndex]; 99 | if (!section.isSectionExpanded()) { 100 | return Container(); 101 | } 102 | return Container( 103 | height: 300, 104 | child: NotificationListener( 105 | onNotification: _onNotification, 106 | child: ListView.builder( 107 | physics: AlwaysScrollableScrollPhysics(), 108 | itemBuilder: containerInfo.childDelegate!.builder as Widget Function( 109 | BuildContext, int), 110 | itemCount: containerInfo.childDelegate!.childCount, 111 | ), 112 | ), 113 | ); 114 | } 115 | 116 | bool _onNotification(ScrollNotification notification) { 117 | var metrics = notification.metrics; 118 | if (notification is ScrollEndNotification) { 119 | drag = null; 120 | } 121 | if (metrics.axis == Axis.horizontal) { 122 | return true; 123 | } 124 | if (notification is ScrollStartNotification) { 125 | drag = null; 126 | dragStartDetails = notification.dragDetails; 127 | } 128 | if (notification is UserScrollNotification) { 129 | if (metrics.pixels <= metrics.minScrollExtent) { 130 | if (drag == null && dragStartDetails != null) { 131 | drag = _scrollListener.position.drag(dragStartDetails!, () { 132 | drag = null; 133 | }); 134 | } 135 | } else if (metrics.pixels >= metrics.maxScrollExtent) { 136 | if (drag == null && dragStartDetails != null) { 137 | drag = _scrollListener.position.drag(dragStartDetails!, () { 138 | drag = null; 139 | }); 140 | } 141 | } 142 | } 143 | return true; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /example/lib/example_pull_to_refresh.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/mock_data.dart'; 2 | import 'package:example/widgets.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/physics.dart'; 6 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 7 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 8 | 9 | //use library 'pull_to_refresh' 10 | //https://github.com/peng8350/flutter_pulltorefresh 11 | class ExamplePullToRefresh extends StatefulWidget { 12 | @override 13 | _ExamplePullToRefreshState createState() => _ExamplePullToRefreshState(); 14 | } 15 | 16 | class _ExamplePullToRefreshState extends State { 17 | var sectionList = MockData.getExampleSections(5, 3); 18 | 19 | RefreshController _refreshController = 20 | RefreshController(initialRefresh: false); 21 | 22 | void _onRefresh() async { 23 | // monitor network fetch 24 | await Future.delayed(Duration(milliseconds: 1000)); 25 | sectionList = MockData.getExampleSections(5, 3); 26 | if (mounted) setState(() {}); 27 | // if failed,use refreshFailed() 28 | _refreshController.refreshCompleted(); 29 | } 30 | 31 | void _onLoading() async { 32 | // monitor network fetch 33 | await Future.delayed(Duration(milliseconds: 1000)); 34 | // if failed,use loadFailed(),if no data return,use LoadNodata() 35 | sectionList.addAll(MockData.getExampleSections(1, 1)); 36 | if (mounted) setState(() {}); 37 | _refreshController.loadComplete(); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return RefreshConfiguration( 43 | headerBuilder: () => WaterDropHeader(), 44 | // Configure the default header indicator. If you have the same header indicator for each page, you need to set this 45 | footerBuilder: () => ClassicFooter(), 46 | // Configure default bottom indicator 47 | headerTriggerDistance: 80.0, 48 | // header trigger refresh trigger distance 49 | springDescription: 50 | SpringDescription(stiffness: 170, damping: 16, mass: 1.9), 51 | // custom spring back animate,the props meaning see the flutter api 52 | maxOverScrollExtent: 100, 53 | //The maximum dragging range of the head. Set this property if a rush out of the view area occurs 54 | maxUnderScrollExtent: 0, 55 | // Maximum dragging range at the bottom 56 | enableScrollWhenRefreshCompleted: true, 57 | //This property is incompatible with PageView and TabBarView. If you need TabBarView to slide left and right, you need to set it to true. 58 | enableLoadingWhenFailed: true, 59 | //In the case of load failure, users can still trigger more loads by gesture pull-up. 60 | hideFooterWhenNotFull: false, 61 | // Disable pull-up to load more functionality when Viewport is less than one screen 62 | enableBallisticLoad: true, 63 | // trigger load more by BallisticScrollActivity 64 | child: Scaffold( 65 | appBar: CustomAppBar(title: "PullToRefresh Example"), 66 | body: SmartRefresher( 67 | enablePullDown: true, 68 | enablePullUp: true, 69 | header: ClassicHeader(), 70 | footer: CustomFooter( 71 | builder: (BuildContext context, LoadStatus? mode) { 72 | Widget body; 73 | if (mode == LoadStatus.idle) { 74 | body = Text("pull up load"); 75 | } else if (mode == LoadStatus.loading) { 76 | body = CupertinoActivityIndicator(); 77 | } else if (mode == LoadStatus.failed) { 78 | body = Text("Load Failed!Click retry!"); 79 | } else if (mode == LoadStatus.canLoading) { 80 | body = Text("release to load more"); 81 | } else { 82 | body = Text("No more Data"); 83 | } 84 | return Container( 85 | height: 55.0, 86 | child: Center(child: body), 87 | ); 88 | }, 89 | ), 90 | controller: _refreshController, 91 | onRefresh: _onRefresh, 92 | onLoading: _onLoading, 93 | child: buildListView(context), 94 | ), 95 | ), 96 | ); 97 | } 98 | 99 | Widget buildListView(BuildContext context) { 100 | //In this example, we create a custom model class(ExampleSection). 101 | //class ExampleSection implements ExpandableListSection {} 102 | //so: SliverExpandableChildDelegate() 103 | return ExpandableListView( 104 | builder: SliverExpandableChildDelegate( 105 | sectionList: sectionList, 106 | headerBuilder: _buildHeader, 107 | itemBuilder: (context, sectionIndex, itemIndex, index) { 108 | String item = sectionList[sectionIndex].items[itemIndex]; 109 | return ListTile( 110 | leading: CircleAvatar( 111 | child: Text("$index"), 112 | ), 113 | title: Text(item), 114 | ); 115 | }), 116 | ); 117 | } 118 | 119 | Widget _buildHeader(BuildContext context, int sectionIndex, int index) { 120 | ExampleSection section = sectionList[sectionIndex]; 121 | return InkWell( 122 | child: Container( 123 | color: Colors.lightBlue, 124 | height: 48, 125 | padding: EdgeInsets.only(left: 20), 126 | alignment: Alignment.centerLeft, 127 | child: Text( 128 | section.header, 129 | style: TextStyle(color: Colors.white), 130 | )), 131 | onTap: () { 132 | //toggle section expand state 133 | setState(() { 134 | section.setSectionExpanded(!section.isSectionExpanded()); 135 | }); 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /example/lib/example_nested_scroll_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:sticky_and_expandable_list/sticky_and_expandable_list.dart'; 3 | 4 | import 'mock_data.dart'; 5 | 6 | class ExampleNestedScrollView extends StatefulWidget { 7 | @override 8 | _ExampleNestedScrollViewState createState() => 9 | _ExampleNestedScrollViewState(); 10 | } 11 | 12 | class _ExampleNestedScrollViewState extends State 13 | with TickerProviderStateMixin { 14 | var sectionList = MockData.getExampleSections(); 15 | late TabController tabController, subTabController; 16 | final GlobalKey nestedScrollKey = GlobalKey(); 17 | double _expandedHeight = 200; 18 | 19 | bool _isPinnedTitleShown = false; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | this.tabController = TabController(length: 2, vsync: this); 25 | this.subTabController = TabController(length: 2, vsync: this); 26 | var headerContentHeight = _expandedHeight - kToolbarHeight; 27 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) { 28 | outerController.addListener(() { 29 | var pinned = outerController.offset >= headerContentHeight; 30 | if (_isPinnedTitleShown != pinned) { 31 | setState(() { 32 | _isPinnedTitleShown = pinned; 33 | }); 34 | } 35 | // print("outerController position: $outerController $kToolbarHeight"); 36 | }); 37 | }); 38 | } 39 | 40 | @override 41 | void dispose() { 42 | tabController.dispose(); 43 | subTabController.dispose(); 44 | super.dispose(); 45 | } 46 | 47 | ScrollController get outerController { 48 | return nestedScrollKey.currentState!.outerController; 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Scaffold( 54 | body: NestedScrollView( 55 | key: nestedScrollKey, 56 | headerSliverBuilder: (context, innerBoxIsScrolled) { 57 | return [ 58 | SliverAppBar( 59 | backgroundColor: Colors.white, 60 | pinned: false, 61 | expandedHeight: _expandedHeight, 62 | flexibleSpace: FlexibleSpaceBar( 63 | title: Text( 64 | "Appbar top area", 65 | style: TextStyle(color: Colors.black87), 66 | ), 67 | ), 68 | ), 69 | // ), 70 | ]; 71 | }, 72 | body: CustomScrollView( 73 | slivers: [ 74 | SliverAppBar( 75 | backgroundColor: Colors.white, 76 | pinned: true, 77 | elevation: 0, 78 | title: Text( 79 | _isPinnedTitleShown ? "PinnedTitle" : "", 80 | style: TextStyle(color: Colors.black), 81 | ), 82 | bottom: TabBar( 83 | labelColor: Colors.black, 84 | controller: this.tabController, 85 | tabs: [ 86 | Tab(text: 'Home'), 87 | Tab(text: 'Profile'), 88 | ], 89 | ), 90 | ), 91 | SliverPersistentHeader( 92 | // 可以吸顶的TabBar 93 | pinned: true, 94 | delegate: StickyTabBarDelegate( 95 | child: TabBar( 96 | labelColor: Colors.black, 97 | controller: this.subTabController, 98 | tabs: [ 99 | Tab(text: 'SubTab1'), 100 | Tab(text: 'SubTab2'), 101 | ], 102 | ), 103 | ), 104 | ), 105 | SliverToBoxAdapter( 106 | child: Container( 107 | height: 300, 108 | child: TabBarView( 109 | controller: this.subTabController, 110 | children: [ 111 | Center(child: Text('Content of SubTab1')), 112 | Center(child: Text('Content of SubTab2')), 113 | ], 114 | ), 115 | ), 116 | ), 117 | // SliverFillRemaining( 118 | // // 剩余补充内容TabBarView 119 | // child: TabBarView( 120 | // controller: this.tabController, 121 | // children: [ 122 | // Center(child: Text('Content of Home')), 123 | // Center(child: Text('Content of Profile')), 124 | // ], 125 | // ), 126 | // ), 127 | SliverExpandableList( 128 | builder: SliverExpandableChildDelegate( 129 | sectionList: sectionList, 130 | headerBuilder: _buildHeader, 131 | itemBuilder: (context, sectionIndex, itemIndex, index) { 132 | String item = sectionList[sectionIndex].items[itemIndex]; 133 | return ListTile( 134 | leading: CircleAvatar( 135 | child: Text("$index"), 136 | ), 137 | title: Text(item), 138 | ); 139 | }, 140 | ), 141 | ), 142 | ], 143 | ), 144 | ), 145 | ); 146 | } 147 | 148 | Widget _buildHeader(BuildContext context, int sectionIndex, int index) { 149 | ExampleSection section = sectionList[sectionIndex]; 150 | return InkWell( 151 | child: Container( 152 | color: Colors.lightBlue, 153 | height: 48, 154 | padding: EdgeInsets.only(left: 20), 155 | alignment: Alignment.centerLeft, 156 | child: Text( 157 | "Header #$sectionIndex", 158 | style: TextStyle(color: Colors.white), 159 | )), 160 | onTap: () { 161 | //toggle section expand state 162 | setState(() { 163 | section.setSectionExpanded(!section.isSectionExpanded()); 164 | }); 165 | }); 166 | } 167 | } 168 | 169 | class StickyTabBarDelegate extends SliverPersistentHeaderDelegate { 170 | final TabBar child; 171 | 172 | StickyTabBarDelegate({required this.child}); 173 | 174 | @override 175 | Widget build( 176 | BuildContext context, double shrinkOffset, bool overlapsContent) { 177 | // print("shrinkOffset:$shrinkOffset overlapsContent:$overlapsContent"); 178 | return Container(color: Colors.yellow, child: this.child); 179 | } 180 | 181 | @override 182 | double get maxExtent => this.child.preferredSize.height; 183 | 184 | @override 185 | double get minExtent => this.child.preferredSize.height; 186 | 187 | @override 188 | bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) { 189 | return true; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /lib/src/sliver_expandable_list.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/rendering.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import '../sticky_and_expandable_list.dart'; 8 | 9 | typedef ExpandableHeaderBuilder = Widget Function( 10 | BuildContext context, int sectionIndex, int index); 11 | typedef ExpandableItemBuilder = Widget Function( 12 | BuildContext context, int sectionIndex, int itemIndex, int index); 13 | typedef ExpandableSeparatorBuilder = Widget Function( 14 | BuildContext context, bool isSectionSeparator, int index); 15 | typedef ExpandableSectionBuilder = Widget Function( 16 | BuildContext context, ExpandableSectionContainerInfo containerInfo); 17 | 18 | /// A scrollable list of widgets arranged linearly, support expand/collapse item and 19 | /// sticky header. 20 | /// all build options are set in [SliverExpandableChildDelegate], this is to avoid 21 | /// [SliverExpandableList] use generics. 22 | class SliverExpandableList extends SliverList { 23 | final SliverExpandableChildDelegate builder; 24 | 25 | SliverExpandableList({ 26 | Key? key, 27 | required this.builder, 28 | }) : super(key: key, delegate: builder.delegate); 29 | 30 | @override 31 | RenderSliverList createRenderObject(BuildContext context) { 32 | final SliverMultiBoxAdaptorElement element = 33 | context as SliverMultiBoxAdaptorElement; 34 | return RenderExpandableSliverList(childManager: element) 35 | ..expandStateList = _buildExpandStateList(); 36 | } 37 | 38 | @override 39 | void updateRenderObject( 40 | BuildContext context, RenderExpandableSliverList renderObject) { 41 | var oldRenderList = renderObject.expandStateList; 42 | renderObject.expandStateList = _buildExpandStateList(); 43 | if (!renderObject.sizeChanged && 44 | listEquals(oldRenderList, renderObject.expandStateList)) { 45 | renderObject.sizeChanged = true; 46 | } 47 | super.updateRenderObject(context, renderObject); 48 | } 49 | 50 | List _buildExpandStateList() { 51 | List sectionList = builder.sectionList; 52 | return List.generate( 53 | sectionList.length, (index) => sectionList[index].isSectionExpanded()); 54 | } 55 | } 56 | 57 | class RenderExpandableSliverList extends RenderSliverList { 58 | /// Creates a sliver that places multiple box children in a linear array along 59 | /// the main axis. 60 | /// 61 | /// The [childManager] argument must not be null. 62 | 63 | List expandStateList = []; 64 | bool sizeChanged = false; 65 | 66 | RenderExpandableSliverList({ 67 | required RenderSliverBoxChildManager childManager, 68 | }) : super(childManager: childManager); 69 | 70 | @override 71 | void performLayout() { 72 | super.performLayout(); 73 | sizeChanged = false; 74 | } 75 | } 76 | 77 | /// A delegate that supplies children for [SliverExpandableList] using 78 | /// a builder callback. 79 | class SliverExpandableChildDelegate> { 80 | ///data source 81 | final List sectionList; 82 | 83 | ///build section header 84 | final ExpandableHeaderBuilder? headerBuilder; 85 | 86 | ///build section item 87 | final ExpandableItemBuilder itemBuilder; 88 | 89 | ///build header and item separator, if pass null, SliverList has no separators. 90 | ///default null. 91 | final ExpandableSeparatorBuilder? separatorBuilder; 92 | 93 | ///whether to sticky the header. 94 | final bool sticky; 95 | 96 | /// Whether the header should be drawn on top of the content 97 | /// instead of before. 98 | final bool overlapsContent; 99 | 100 | ///store section real index in SliverList, format: [sectionList index, SliverList index]. 101 | final List sectionRealIndexes; 102 | 103 | ///use this return a custom content widget, when use this builder, headerBuilder 104 | ///is invalid. 105 | /// See also: 106 | /// 107 | /// * , 108 | /// a description of what ExpandableSectionBuilder are and how to use it. 109 | /// 110 | ExpandableSectionBuilder? sectionBuilder; 111 | 112 | ///expandable list controller, listen sticky header index scroll offset etc. 113 | ExpandableListController? controller; 114 | 115 | ///sliver list builder 116 | late SliverChildBuilderDelegate delegate; 117 | 118 | ///if value is true, when section is collapsed, all child widget in section widget will be removed. 119 | @Deprecated("unused property") 120 | final bool removeItemsOnCollapsed; 121 | 122 | SliverExpandableChildDelegate( 123 | {required this.sectionList, 124 | required this.itemBuilder, 125 | this.controller, 126 | this.separatorBuilder, 127 | this.headerBuilder, 128 | this.sectionBuilder, 129 | this.sticky = true, 130 | this.overlapsContent = false, 131 | this.removeItemsOnCollapsed = true, 132 | bool addAutomaticKeepAlives = true, 133 | bool addRepaintBoundaries = true, 134 | bool addSemanticIndexes = true}) 135 | : assert( 136 | (headerBuilder != null && sectionBuilder == null) || 137 | (headerBuilder == null && sectionBuilder != null), 138 | 'You must specify either headerBuilder or sectionBuilder.', 139 | ), 140 | sectionRealIndexes = _buildSectionRealIndexes(sectionList) { 141 | if (controller == null) { 142 | controller = ExpandableListController(); 143 | } 144 | if (separatorBuilder == null) { 145 | delegate = SliverChildBuilderDelegate( 146 | (BuildContext context, int index) { 147 | int sectionIndex = index; 148 | S section = sectionList[sectionIndex]; 149 | int sectionRealIndex = sectionRealIndexes[sectionIndex]; 150 | 151 | int sectionChildCount = section.getItems()?.length ?? 0; 152 | if (!section.isSectionExpanded()) { 153 | sectionChildCount = 0; 154 | } 155 | var childBuilderDelegate = SliverChildBuilderDelegate( 156 | (context, i) => itemBuilder( 157 | context, sectionIndex, i, sectionRealIndex + i + 1), 158 | childCount: sectionChildCount); 159 | var containerInfo = ExpandableSectionContainerInfo( 160 | separated: false, 161 | listIndex: index, 162 | sectionIndex: sectionIndex, 163 | sectionRealIndexes: sectionRealIndexes, 164 | sticky: sticky, 165 | overlapsContent: overlapsContent, 166 | controller: controller!, 167 | header: Container(), 168 | content: Container(), 169 | childDelegate: childBuilderDelegate, 170 | ); 171 | Widget? container = sectionBuilder != null 172 | ? sectionBuilder!(context, containerInfo) 173 | : null; 174 | if (container == null) { 175 | containerInfo 176 | ..header = headerBuilder!(context, sectionIndex, sectionRealIndex) 177 | ..content = buildDefaultContent(context, containerInfo); 178 | container = ExpandableSectionContainer( 179 | info: containerInfo, 180 | ); 181 | } 182 | assert(containerInfo.header != null); 183 | assert(containerInfo.content != null); 184 | return container; 185 | }, 186 | childCount: sectionList.length, 187 | addAutomaticKeepAlives: addAutomaticKeepAlives, 188 | addRepaintBoundaries: addRepaintBoundaries, 189 | addSemanticIndexes: addSemanticIndexes, 190 | ); 191 | } else { 192 | delegate = SliverChildBuilderDelegate( 193 | (BuildContext context, int index) { 194 | final int sectionIndex = index ~/ 2; 195 | Widget itemView; 196 | S section = sectionList[sectionIndex]; 197 | int sectionRealIndex = sectionRealIndexes[sectionIndex]; 198 | if (index.isEven) { 199 | int sectionChildCount = 200 | _computeSemanticChildCount(section.getItems()?.length ?? 0); 201 | if (!section.isSectionExpanded()) { 202 | sectionChildCount = 0; 203 | } 204 | var childBuilderDelegate = SliverChildBuilderDelegate((context, i) { 205 | int itemRealIndex = sectionRealIndex + (i ~/ 2) + 1; 206 | if (i.isEven) { 207 | return itemBuilder( 208 | context, sectionIndex, i ~/ 2, itemRealIndex); 209 | } else { 210 | return separatorBuilder!(context, false, itemRealIndex); 211 | } 212 | }, childCount: sectionChildCount); 213 | var containerInfo = ExpandableSectionContainerInfo( 214 | separated: true, 215 | listIndex: index, 216 | sectionIndex: sectionIndex, 217 | sectionRealIndexes: sectionRealIndexes, 218 | sticky: sticky, 219 | overlapsContent: overlapsContent, 220 | controller: controller!, 221 | header: Container(), 222 | content: Container(), 223 | childDelegate: childBuilderDelegate, 224 | ); 225 | Widget? container = sectionBuilder != null 226 | ? sectionBuilder!(context, containerInfo) 227 | : null; 228 | if (container == null) { 229 | containerInfo 230 | ..header = 231 | headerBuilder!(context, sectionIndex, sectionRealIndex) 232 | ..content = buildDefaultContent(context, containerInfo); 233 | container = ExpandableSectionContainer( 234 | info: containerInfo, 235 | ); 236 | } 237 | assert(containerInfo.header != null); 238 | assert(containerInfo.content != null); 239 | return container; 240 | } else { 241 | itemView = separatorBuilder!(context, true, 242 | sectionIndex + (section.getItems()?.length ?? 0)); 243 | } 244 | return itemView; 245 | }, 246 | childCount: _computeSemanticChildCount(sectionList.length), 247 | addAutomaticKeepAlives: addAutomaticKeepAlives, 248 | addRepaintBoundaries: addRepaintBoundaries, 249 | addSemanticIndexes: addSemanticIndexes, 250 | semanticIndexCallback: (Widget _, int index) { 251 | return index.isEven ? index ~/ 2 : null; 252 | }, 253 | ); 254 | } 255 | } 256 | 257 | ///By default, build a Column widget for layout all children's size. 258 | static Widget buildDefaultContent( 259 | BuildContext context, ExpandableSectionContainerInfo containerInfo) { 260 | var childDelegate = containerInfo.childDelegate; 261 | if (childDelegate != null) { 262 | var children = 263 | List.generate(childDelegate.childCount ?? 0, (index) { 264 | return childDelegate.builder(context, index) ?? Container(); 265 | }); 266 | return Column( 267 | children: children, 268 | ); 269 | } 270 | return Container(); 271 | } 272 | 273 | static int _computeSemanticChildCount(int itemCount) { 274 | return math.max(0, itemCount * 2 - 1); 275 | } 276 | 277 | static List 278 | _buildSectionRealIndexes>( 279 | List sectionList) { 280 | int calcLength = sectionList.length - 1; 281 | List sectionRealIndexes = List.empty(growable: true); 282 | if (calcLength < 0) { 283 | return sectionRealIndexes; 284 | } 285 | sectionRealIndexes.add(0); 286 | int realIndex = 0; 287 | for (int i = 0; i < calcLength; i++) { 288 | S section = sectionList[i]; 289 | //each section model should not null. 290 | realIndex += 1 + (section.getItems()?.length ?? 0); 291 | sectionRealIndexes.add(realIndex); 292 | } 293 | return sectionRealIndexes; 294 | } 295 | } 296 | 297 | ///Used to provide information for each section, each section model 298 | ///should implement [ExpandableListSection]. 299 | abstract class ExpandableListSection { 300 | bool isSectionExpanded(); 301 | 302 | void setSectionExpanded(bool expanded); 303 | 304 | List? getItems(); 305 | } 306 | 307 | ///Controller for listen sticky header offset and current sticky header index. 308 | class ExpandableListController extends ChangeNotifier { 309 | ///switchingSection scroll percent, [0.1-1.0], 1.0 mean that the last sticky section 310 | ///is completely hidden. 311 | double _percent = 1.0; 312 | int _switchingSectionIndex = -1; 313 | int _stickySectionIndex = -1; 314 | 315 | ExpandableListController(); 316 | 317 | ///store [ExpandableSectionContainer] information. [SliverList index, layoutOffset]. 318 | ///don't modify it. 319 | List containerOffsets = []; 320 | 321 | double get percent => _percent; 322 | 323 | int get switchingSectionIndex => _switchingSectionIndex; 324 | 325 | ///get pinned header index 326 | int get stickySectionIndex => _stickySectionIndex; 327 | 328 | updatePercent(int sectionIndex, double percent) { 329 | if (_percent == percent && _switchingSectionIndex == sectionIndex) { 330 | return; 331 | } 332 | _switchingSectionIndex = sectionIndex; 333 | _percent = percent; 334 | notifyListeners(); 335 | } 336 | 337 | set stickySectionIndex(int value) { 338 | if (_stickySectionIndex == value) { 339 | return; 340 | } 341 | _stickySectionIndex = value; 342 | notifyListeners(); 343 | } 344 | 345 | void forceNotifyListeners() { 346 | notifyListeners(); 347 | } 348 | 349 | @override 350 | String toString() { 351 | return 'ExpandableListController{_percent: $_percent, _switchingSectionIndex: $_switchingSectionIndex, _stickySectionIndex: $_stickySectionIndex} #$hashCode'; 352 | } 353 | } 354 | 355 | ///Check if need rebuild [ExpandableAutoLayoutWidget] 356 | abstract class ExpandableAutoLayoutTrigger { 357 | ExpandableListController get controller; 358 | 359 | bool needBuild(); 360 | } 361 | 362 | ///Default [ExpandableAutoLayoutTrigger] implementation, auto build when 363 | ///switch sticky header index. 364 | class ExpandableDefaultAutoLayoutTrigger 365 | implements ExpandableAutoLayoutTrigger { 366 | final ExpandableListController _controller; 367 | 368 | double _percent = 0; 369 | int _stickyIndex = 0; 370 | 371 | ExpandableDefaultAutoLayoutTrigger(this._controller) : super(); 372 | 373 | @override 374 | bool needBuild() { 375 | if (_percent == _controller.percent && 376 | _stickyIndex == _controller.stickySectionIndex) { 377 | return false; 378 | } 379 | _percent = _controller.percent; 380 | _stickyIndex = _controller.stickySectionIndex; 381 | return true; 382 | } 383 | 384 | @override 385 | ExpandableListController get controller => _controller; 386 | } 387 | 388 | ///Wrap header widget, when controller is set, the widget will rebuild 389 | ///when [trigger] condition matched. 390 | class ExpandableAutoLayoutWidget extends StatefulWidget { 391 | ///listen sticky header hide percent, [0.0-0.1]. 392 | final ExpandableAutoLayoutTrigger trigger; 393 | 394 | ///build section header 395 | final WidgetBuilder builder; 396 | 397 | ExpandableAutoLayoutWidget({required this.builder, required this.trigger}); 398 | 399 | @override 400 | _ExpandableAutoLayoutWidgetState createState() => 401 | _ExpandableAutoLayoutWidgetState(); 402 | } 403 | 404 | class _ExpandableAutoLayoutWidgetState 405 | extends State { 406 | @override 407 | void initState() { 408 | super.initState(); 409 | widget.trigger.controller.addListener(_onChange); 410 | } 411 | 412 | void _onChange() { 413 | if (widget.trigger.needBuild()) { 414 | WidgetsBinding.instance.addPostFrameCallback((_) { 415 | if (mounted) { 416 | setState(() {}); 417 | } 418 | }); 419 | } 420 | } 421 | 422 | @override 423 | void dispose() { 424 | widget.trigger.controller.removeListener(_onChange); 425 | super.dispose(); 426 | } 427 | 428 | @override 429 | Widget build(BuildContext context) { 430 | return RepaintBoundary( 431 | child: widget.builder(context), 432 | ); 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /lib/src/expandable_section_container.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:flutter/rendering.dart'; 5 | import '../sticky_and_expandable_list.dart'; 6 | 7 | ///Section widget information. 8 | class ExpandableSectionContainerInfo { 9 | Widget? header; 10 | Widget? content; 11 | final SliverChildBuilderDelegate? childDelegate; 12 | final int listIndex; 13 | final List sectionRealIndexes; 14 | final bool separated; 15 | 16 | final ExpandableListController controller; 17 | final int sectionIndex; 18 | final bool sticky; 19 | final bool overlapsContent; 20 | 21 | ExpandableSectionContainerInfo( 22 | {this.header, 23 | this.content, 24 | this.childDelegate, 25 | required this.listIndex, 26 | required this.sectionRealIndexes, 27 | required this.separated, 28 | required this.controller, 29 | required this.sectionIndex, 30 | required this.sticky, 31 | required this.overlapsContent}); 32 | 33 | @override 34 | bool operator ==(Object other) => 35 | identical(this, other) || 36 | other is ExpandableSectionContainerInfo && 37 | runtimeType == other.runtimeType && 38 | header == other.header && 39 | content == other.content && 40 | childDelegate == other.childDelegate && 41 | listIndex == other.listIndex && 42 | sectionRealIndexes == other.sectionRealIndexes && 43 | separated == other.separated && 44 | controller == other.controller && 45 | sectionIndex == other.sectionIndex && 46 | sticky == other.sticky && 47 | overlapsContent == other.overlapsContent; 48 | 49 | @override 50 | int get hashCode => 51 | (header?.hashCode ?? 0) ^ 52 | (content?.hashCode ?? 0) ^ 53 | (childDelegate?.hashCode ?? 0) ^ 54 | listIndex.hashCode ^ 55 | sectionRealIndexes.hashCode ^ 56 | separated.hashCode ^ 57 | controller.hashCode ^ 58 | sectionIndex.hashCode ^ 59 | sticky.hashCode ^ 60 | overlapsContent.hashCode; 61 | } 62 | 63 | ///Section widget that contains header and content widget. 64 | ///You can return a custom [ExpandableSectionContainer] 65 | ///by [SliverExpandableChildDelegate.sectionBuilder], but only 66 | ///[header] and [content] field could be changed. 67 | /// 68 | class ExpandableSectionContainer extends MultiChildRenderObjectWidget { 69 | final ExpandableSectionContainerInfo info; 70 | 71 | ExpandableSectionContainer({ 72 | Key? key, 73 | required this.info, 74 | }) : super(key: key, children: [info.content!, info.header!]); 75 | 76 | @override 77 | RenderExpandableSectionContainer createRenderObject(BuildContext context) { 78 | var renderSliver = 79 | context.findAncestorRenderObjectOfType()!; 80 | return RenderExpandableSectionContainer( 81 | renderSliver: renderSliver, 82 | scrollable: Scrollable.of(context)!, 83 | controller: this.info.controller, 84 | sticky: this.info.sticky, 85 | overlapsContent: this.info.overlapsContent, 86 | listIndex: this.info.listIndex, 87 | sectionRealIndexes: this.info.sectionRealIndexes, 88 | separated: this.info.separated, 89 | ); 90 | } 91 | 92 | @override 93 | void updateRenderObject( 94 | BuildContext context, RenderExpandableSectionContainer renderObject) { 95 | renderObject 96 | ..scrollable = Scrollable.of(context)! 97 | ..controller = this.info.controller 98 | ..sticky = this.info.sticky 99 | ..overlapsContent = this.info.overlapsContent 100 | ..listIndex = this.info.listIndex 101 | ..sectionRealIndexes = this.info.sectionRealIndexes 102 | ..separated = this.info.separated; 103 | } 104 | } 105 | 106 | ///Render [ExpandableSectionContainer] 107 | class RenderExpandableSectionContainer extends RenderBox 108 | with 109 | ContainerRenderObjectMixin, 110 | RenderBoxContainerDefaultsMixin { 111 | static const String TAG = "ExpandableSectionContainer"; 112 | bool _sticky; 113 | bool _overlapsContent; 114 | ScrollableState _scrollable; 115 | ExpandableListController _controller; 116 | RenderExpandableSliverList _renderSliver; 117 | int _listIndex; 118 | int _stickyIndex = -1; 119 | 120 | ///[sectionIndex, section in SliverList index]. 121 | List _sectionRealIndexes; 122 | 123 | /// is SliverList has separator 124 | bool _separated; 125 | 126 | RenderExpandableSectionContainer({ 127 | required ScrollableState scrollable, 128 | required ExpandableListController controller, 129 | sticky = true, 130 | overlapsContent = false, 131 | int listIndex = -1, 132 | List sectionRealIndexes = const [], 133 | bool separated = false, 134 | required RenderExpandableSliverList renderSliver, 135 | }) : _scrollable = scrollable, 136 | _controller = controller, 137 | _sticky = sticky, 138 | _overlapsContent = overlapsContent, 139 | _listIndex = listIndex, 140 | _sectionRealIndexes = sectionRealIndexes, 141 | _separated = separated, 142 | _renderSliver = renderSliver; 143 | 144 | List get sectionRealIndexes => _sectionRealIndexes; 145 | 146 | set sectionRealIndexes(List value) { 147 | if (_sectionRealIndexes == value) { 148 | return; 149 | } 150 | _sectionRealIndexes = value; 151 | markNeedsLayout(); 152 | } 153 | 154 | bool get separated => _separated; 155 | 156 | set separated(bool value) { 157 | if (_separated == value) { 158 | return; 159 | } 160 | _separated = value; 161 | markNeedsLayout(); 162 | } 163 | 164 | ScrollableState get scrollable => _scrollable; 165 | 166 | set scrollable(ScrollableState value) { 167 | // print("$TAG update scrollable: ${_renderSliver.sizeChanged}"); 168 | 169 | //when collapse last section, Sliver list not callback correct offset, so layout again. 170 | if (_renderSliver.sizeChanged) { 171 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) { 172 | if (attached) { 173 | clearContainerLayoutOffsets(); 174 | markNeedsLayout(); 175 | } 176 | }); 177 | } 178 | if (_scrollable == value) { 179 | return; 180 | } 181 | final ScrollableState oldValue = _scrollable; 182 | _scrollable = value; 183 | markNeedsLayout(); 184 | if (attached) { 185 | oldValue.widget.controller?.removeListener(markNeedsLayout); 186 | if (_sticky) { 187 | _scrollable.widget.controller?.addListener(markNeedsLayout); 188 | } 189 | } 190 | } 191 | 192 | ExpandableListController get controller => _controller; 193 | 194 | set controller(ExpandableListController value) { 195 | if (_controller == value) { 196 | return; 197 | } 198 | _controller = value; 199 | markNeedsLayout(); 200 | } 201 | 202 | bool get sticky => _sticky; 203 | 204 | set sticky(bool value) { 205 | if (_sticky == value) { 206 | return; 207 | } 208 | _sticky = value; 209 | markNeedsLayout(); 210 | if (attached && !_sticky) { 211 | _scrollable.widget.controller?.removeListener(markNeedsLayout); 212 | } 213 | } 214 | 215 | bool get overlapsContent => _overlapsContent; 216 | 217 | set overlapsContent(bool value) { 218 | if (_overlapsContent == value) { 219 | return; 220 | } 221 | _overlapsContent = value; 222 | markNeedsLayout(); 223 | } 224 | 225 | int get listIndex => _listIndex; 226 | 227 | set listIndex(int value) { 228 | if (_listIndex == value) { 229 | return; 230 | } 231 | _listIndex = value; 232 | markNeedsLayout(); 233 | } 234 | 235 | @override 236 | void setupParentData(RenderBox child) { 237 | if (child.parentData is! MultiChildLayoutParentData) 238 | child.parentData = MultiChildLayoutParentData(); 239 | } 240 | 241 | @override 242 | void attach(PipelineOwner owner) { 243 | super.attach(owner); 244 | if (sticky) { 245 | _scrollable.widget.controller?.addListener(markNeedsLayout); 246 | } 247 | } 248 | 249 | @override 250 | void detach() { 251 | _scrollable.widget.controller?.removeListener(markNeedsLayout); 252 | super.detach(); 253 | } 254 | 255 | RenderBox get content => firstChild!; 256 | 257 | RenderBox get header => lastChild!; 258 | 259 | @override 260 | double computeMinIntrinsicWidth(double height) { 261 | return _overlapsContent 262 | ? max(header.getMinIntrinsicWidth(height), 263 | content.getMinIntrinsicWidth(height)) 264 | : header.getMinIntrinsicWidth(height) + 265 | content.getMinIntrinsicWidth(height); 266 | } 267 | 268 | @override 269 | double computeMaxIntrinsicWidth(double height) { 270 | return _overlapsContent 271 | ? max(header.getMaxIntrinsicWidth(height), 272 | content.getMaxIntrinsicWidth(height)) 273 | : header.getMaxIntrinsicWidth(height) + 274 | content.getMaxIntrinsicWidth(height); 275 | } 276 | 277 | @override 278 | double computeMinIntrinsicHeight(double width) { 279 | return _overlapsContent 280 | ? max(header.getMinIntrinsicHeight(width), 281 | content.getMinIntrinsicHeight(width)) 282 | : header.getMinIntrinsicHeight(width) + 283 | content.getMinIntrinsicHeight(width); 284 | } 285 | 286 | @override 287 | double computeMaxIntrinsicHeight(double width) { 288 | return _overlapsContent 289 | ? max(header.getMaxIntrinsicHeight(width), 290 | content.getMaxIntrinsicHeight(width)) 291 | : header.getMaxIntrinsicHeight(width) + 292 | content.getMaxIntrinsicHeight(width); 293 | } 294 | 295 | @override 296 | void performLayout() { 297 | assert(childCount == 2); 298 | 299 | //layout two child 300 | BoxConstraints exactlyConstraints = constraints.loosen(); 301 | header.layout(exactlyConstraints, parentUsesSize: true); 302 | content.layout(exactlyConstraints, parentUsesSize: true); 303 | 304 | //header's size should not large than content's size. 305 | double headerLogicalExtent = _overlapsContent ? 0 : header.size.height; 306 | 307 | double width = 308 | max(constraints.minWidth, max(header.size.width, content.size.width)); 309 | double height = max(constraints.minHeight, 310 | max(header.size.height, headerLogicalExtent + content.size.height)); 311 | size = Size(width, height); 312 | assert(size.width == constraints.constrainWidth(width)); 313 | assert(size.height == constraints.constrainHeight(height)); 314 | 315 | //calc content offset 316 | positionChild(content, Offset(0, headerLogicalExtent)); 317 | 318 | checkRefreshContainerOffset(); 319 | 320 | double sliverListOffset = _getSliverListVisibleScrollOffset(); 321 | double currContainerOffset = -1; 322 | if (_listIndex < _controller.containerOffsets.length) { 323 | currContainerOffset = _controller.containerOffsets[_listIndex]!; 324 | } 325 | bool containerPainted = (_listIndex == 0 && currContainerOffset == 0) || 326 | currContainerOffset > 0; 327 | if (!containerPainted) { 328 | positionChild(header, Offset.zero); 329 | return; 330 | } 331 | double minScrollOffset = _listIndex >= _controller.containerOffsets.length 332 | ? 0 333 | : _controller.containerOffsets[_listIndex]!; 334 | double maxScrollOffset = minScrollOffset + size.height; 335 | 336 | //when [ExpandableSectionContainer] size changed, SliverList may give a wrong 337 | // layoutOffset at first time, so check offsets for store right layoutOffset 338 | // in [containerOffsets]. 339 | if (_listIndex < _controller.containerOffsets.length) { 340 | currContainerOffset = _controller.containerOffsets[_listIndex]!; 341 | int nextListIndex = _listIndex + 1; 342 | if (nextListIndex < _controller.containerOffsets.length && 343 | _controller.containerOffsets[nextListIndex]! < maxScrollOffset) { 344 | _controller.containerOffsets = 345 | _controller.containerOffsets.sublist(0, nextListIndex); 346 | } 347 | } 348 | 349 | if (sliverListOffset > minScrollOffset && 350 | sliverListOffset <= maxScrollOffset) { 351 | if (_stickyIndex != _listIndex) { 352 | _stickyIndex = _listIndex; 353 | _controller.updatePercent(_controller.switchingSectionIndex, 1); 354 | //update sticky index 355 | _controller.stickySectionIndex = sectionIndex; 356 | } 357 | } else if (sliverListOffset <= 0) { 358 | _controller.stickySectionIndex = -1; 359 | _stickyIndex = -1; 360 | } else { 361 | _stickyIndex = -1; 362 | } 363 | 364 | //calc header offset 365 | double currHeaderOffset = 0; 366 | double headerMaxOffset = height - header.size.height; 367 | if (_sticky && isStickyChild && sliverListOffset > minScrollOffset) { 368 | currHeaderOffset = sliverListOffset - minScrollOffset; 369 | } 370 | // print( 371 | // "index:$listIndex currHeaderOffset:${currHeaderOffset.toStringAsFixed(2)}" + 372 | // " sliverListOffset:${sliverListOffset.toStringAsFixed(2)}" + 373 | // " [$minScrollOffset,$maxScrollOffset] size:${content.size.height}"); 374 | positionChild(header, Offset(0, min(currHeaderOffset, headerMaxOffset))); 375 | 376 | //callback header hide percent 377 | if (currHeaderOffset >= headerMaxOffset && currHeaderOffset <= height) { 378 | double switchingPercent = 379 | (currHeaderOffset - headerMaxOffset) / header.size.height; 380 | _controller.updatePercent(sectionIndex, switchingPercent); 381 | } else if (sliverListOffset < minScrollOffset + headerMaxOffset && 382 | _controller.switchingSectionIndex == sectionIndex) { 383 | //ensure callback 0% percent. 384 | _controller.updatePercent(sectionIndex, 0); 385 | //reset switchingSectionIndex 386 | _controller.updatePercent(-1, 1); 387 | } 388 | } 389 | 390 | bool get isStickyChild => _listIndex == _stickyIndex; 391 | 392 | int get sectionIndex => separated ? _listIndex ~/ 2 : _listIndex; 393 | 394 | double _getSliverListVisibleScrollOffset() { 395 | return _renderSliver.constraints.overlap + 396 | _renderSliver.constraints.scrollOffset; 397 | } 398 | 399 | void clearContainerLayoutOffsets() { 400 | // print("$TAG clearContainerLayoutOffsets"); 401 | _controller.containerOffsets.clear(); 402 | } 403 | 404 | void _refreshContainerLayoutOffsets(String reason) { 405 | // print("$TAG _refreshContainerLayoutOffsets reason:$reason"); 406 | _renderSliver.visitChildren((renderObject) { 407 | var containerParentData = 408 | renderObject.parentData as SliverMultiBoxAdaptorParentData; 409 | // print("visitChildren $containerParentData"); 410 | 411 | while ( 412 | _controller.containerOffsets.length <= containerParentData.index!) { 413 | _controller.containerOffsets.add(0); 414 | } 415 | if (containerParentData.layoutOffset != null) { 416 | _controller.containerOffsets[containerParentData.index!] = 417 | containerParentData.layoutOffset; 418 | } 419 | }); 420 | } 421 | 422 | void positionChild(RenderBox child, Offset offset) { 423 | final MultiChildLayoutParentData childParentData = 424 | child.parentData as MultiChildLayoutParentData; 425 | childParentData.offset = offset; 426 | } 427 | 428 | Offset childOffset(RenderBox child) { 429 | final MultiChildLayoutParentData childParentData = 430 | child.parentData as MultiChildLayoutParentData; 431 | return childParentData.offset; 432 | } 433 | 434 | @override 435 | bool get isRepaintBoundary => true; 436 | 437 | @override 438 | void paint(PaintingContext context, Offset offset) { 439 | defaultPaint(context, offset); 440 | } 441 | 442 | @override 443 | bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { 444 | return defaultHitTestChildren(result, position: position); 445 | } 446 | 447 | void checkRefreshContainerOffset() { 448 | int length = _controller.containerOffsets.length; 449 | if (_listIndex >= length || 450 | (_listIndex > 0 && _controller.containerOffsets[_listIndex]! <= 0)) { 451 | _refreshContainerLayoutOffsets("zero size"); 452 | return; 453 | } 454 | for (int i = 0; i < _listIndex && _listIndex < length - 1; i++) { 455 | double currOffset = _controller.containerOffsets[i]?.toDouble() ?? 0; 456 | double nextOffset = _controller.containerOffsets[i + 1]?.toDouble() ?? 0; 457 | if (currOffset > nextOffset) { 458 | _refreshContainerLayoutOffsets("offset invalid: $currOffset->$nextOffset"); 459 | break; 460 | } 461 | } 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 94 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 95 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 96 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 97 | ); 98 | path = Runner; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 97C146ED1CF9000F007C117D /* Runner */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 107 | buildPhases = ( 108 | 9740EEB61CF901F6004384FC /* Run Script */, 109 | 97C146EA1CF9000F007C117D /* Sources */, 110 | 97C146EB1CF9000F007C117D /* Frameworks */, 111 | 97C146EC1CF9000F007C117D /* Resources */, 112 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 113 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = Runner; 120 | productName = Runner; 121 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 97C146E61CF9000F007C117D /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastUpgradeCheck = 1300; 131 | ORGANIZATIONNAME = ""; 132 | TargetAttributes = { 133 | 97C146ED1CF9000F007C117D = { 134 | CreatedOnToolsVersion = 7.3.1; 135 | LastSwiftMigration = 1100; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 97C146E51CF9000F007C117D; 148 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 97C146ED1CF9000F007C117D /* Runner */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 97C146EC1CF9000F007C117D /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 163 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 164 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 165 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | ); 177 | inputPaths = ( 178 | ); 179 | name = "Thin Binary"; 180 | outputPaths = ( 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | shellPath = /bin/sh; 184 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 185 | }; 186 | 9740EEB61CF901F6004384FC /* Run Script */ = { 187 | isa = PBXShellScriptBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | ); 191 | inputPaths = ( 192 | ); 193 | name = "Run Script"; 194 | outputPaths = ( 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 199 | }; 200 | /* End PBXShellScriptBuildPhase section */ 201 | 202 | /* Begin PBXSourcesBuildPhase section */ 203 | 97C146EA1CF9000F007C117D /* Sources */ = { 204 | isa = PBXSourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 208 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXSourcesBuildPhase section */ 213 | 214 | /* Begin PBXVariantGroup section */ 215 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 216 | isa = PBXVariantGroup; 217 | children = ( 218 | 97C146FB1CF9000F007C117D /* Base */, 219 | ); 220 | name = Main.storyboard; 221 | sourceTree = ""; 222 | }; 223 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 97C147001CF9000F007C117D /* Base */, 227 | ); 228 | name = LaunchScreen.storyboard; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXVariantGroup section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_ANALYZER_NONNULL = YES; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 240 | CLANG_CXX_LIBRARY = "libc++"; 241 | CLANG_ENABLE_MODULES = YES; 242 | CLANG_ENABLE_OBJC_ARC = YES; 243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 244 | CLANG_WARN_BOOL_CONVERSION = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu99; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | SDKROOT = iphoneos; 278 | SUPPORTED_PLATFORMS = iphoneos; 279 | TARGETED_DEVICE_FAMILY = "1,2"; 280 | VALIDATE_PRODUCT = YES; 281 | }; 282 | name = Profile; 283 | }; 284 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 285 | isa = XCBuildConfiguration; 286 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | CLANG_ENABLE_MODULES = YES; 290 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 291 | ENABLE_BITCODE = NO; 292 | INFOPLIST_FILE = Runner/Info.plist; 293 | LD_RUNPATH_SEARCH_PATHS = ( 294 | "$(inherited)", 295 | "@executable_path/Frameworks", 296 | ); 297 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 298 | PRODUCT_NAME = "$(TARGET_NAME)"; 299 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 300 | SWIFT_VERSION = 5.0; 301 | VERSIONING_SYSTEM = "apple-generic"; 302 | }; 303 | name = Profile; 304 | }; 305 | 97C147031CF9000F007C117D /* Debug */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ALWAYS_SEARCH_USER_PATHS = NO; 309 | CLANG_ANALYZER_NONNULL = YES; 310 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 311 | CLANG_CXX_LIBRARY = "libc++"; 312 | CLANG_ENABLE_MODULES = YES; 313 | CLANG_ENABLE_OBJC_ARC = YES; 314 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 315 | CLANG_WARN_BOOL_CONVERSION = YES; 316 | CLANG_WARN_COMMA = YES; 317 | CLANG_WARN_CONSTANT_CONVERSION = YES; 318 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 319 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 320 | CLANG_WARN_EMPTY_BODY = YES; 321 | CLANG_WARN_ENUM_CONVERSION = YES; 322 | CLANG_WARN_INFINITE_RECURSION = YES; 323 | CLANG_WARN_INT_CONVERSION = YES; 324 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 326 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 328 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 329 | CLANG_WARN_STRICT_PROTOTYPES = YES; 330 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 331 | CLANG_WARN_UNREACHABLE_CODE = YES; 332 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 333 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 334 | COPY_PHASE_STRIP = NO; 335 | DEBUG_INFORMATION_FORMAT = dwarf; 336 | ENABLE_STRICT_OBJC_MSGSEND = YES; 337 | ENABLE_TESTABILITY = YES; 338 | GCC_C_LANGUAGE_STANDARD = gnu99; 339 | GCC_DYNAMIC_NO_PIC = NO; 340 | GCC_NO_COMMON_BLOCKS = YES; 341 | GCC_OPTIMIZATION_LEVEL = 0; 342 | GCC_PREPROCESSOR_DEFINITIONS = ( 343 | "DEBUG=1", 344 | "$(inherited)", 345 | ); 346 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 347 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 348 | GCC_WARN_UNDECLARED_SELECTOR = YES; 349 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 350 | GCC_WARN_UNUSED_FUNCTION = YES; 351 | GCC_WARN_UNUSED_VARIABLE = YES; 352 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 353 | MTL_ENABLE_DEBUG_INFO = YES; 354 | ONLY_ACTIVE_ARCH = YES; 355 | SDKROOT = iphoneos; 356 | TARGETED_DEVICE_FAMILY = "1,2"; 357 | }; 358 | name = Debug; 359 | }; 360 | 97C147041CF9000F007C117D /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ALWAYS_SEARCH_USER_PATHS = NO; 364 | CLANG_ANALYZER_NONNULL = YES; 365 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 366 | CLANG_CXX_LIBRARY = "libc++"; 367 | CLANG_ENABLE_MODULES = YES; 368 | CLANG_ENABLE_OBJC_ARC = YES; 369 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 370 | CLANG_WARN_BOOL_CONVERSION = YES; 371 | CLANG_WARN_COMMA = YES; 372 | CLANG_WARN_CONSTANT_CONVERSION = YES; 373 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 374 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 375 | CLANG_WARN_EMPTY_BODY = YES; 376 | CLANG_WARN_ENUM_CONVERSION = YES; 377 | CLANG_WARN_INFINITE_RECURSION = YES; 378 | CLANG_WARN_INT_CONVERSION = YES; 379 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 380 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 381 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 382 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 383 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 384 | CLANG_WARN_STRICT_PROTOTYPES = YES; 385 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 386 | CLANG_WARN_UNREACHABLE_CODE = YES; 387 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 388 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 389 | COPY_PHASE_STRIP = NO; 390 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 391 | ENABLE_NS_ASSERTIONS = NO; 392 | ENABLE_STRICT_OBJC_MSGSEND = YES; 393 | GCC_C_LANGUAGE_STANDARD = gnu99; 394 | GCC_NO_COMMON_BLOCKS = YES; 395 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 396 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 397 | GCC_WARN_UNDECLARED_SELECTOR = YES; 398 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 399 | GCC_WARN_UNUSED_FUNCTION = YES; 400 | GCC_WARN_UNUSED_VARIABLE = YES; 401 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 402 | MTL_ENABLE_DEBUG_INFO = NO; 403 | SDKROOT = iphoneos; 404 | SUPPORTED_PLATFORMS = iphoneos; 405 | SWIFT_COMPILATION_MODE = wholemodule; 406 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 407 | TARGETED_DEVICE_FAMILY = "1,2"; 408 | VALIDATE_PRODUCT = YES; 409 | }; 410 | name = Release; 411 | }; 412 | 97C147061CF9000F007C117D /* Debug */ = { 413 | isa = XCBuildConfiguration; 414 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 415 | buildSettings = { 416 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 417 | CLANG_ENABLE_MODULES = YES; 418 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 419 | ENABLE_BITCODE = NO; 420 | INFOPLIST_FILE = Runner/Info.plist; 421 | LD_RUNPATH_SEARCH_PATHS = ( 422 | "$(inherited)", 423 | "@executable_path/Frameworks", 424 | ); 425 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 426 | PRODUCT_NAME = "$(TARGET_NAME)"; 427 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 428 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 429 | SWIFT_VERSION = 5.0; 430 | VERSIONING_SYSTEM = "apple-generic"; 431 | }; 432 | name = Debug; 433 | }; 434 | 97C147071CF9000F007C117D /* Release */ = { 435 | isa = XCBuildConfiguration; 436 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 437 | buildSettings = { 438 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 439 | CLANG_ENABLE_MODULES = YES; 440 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 441 | ENABLE_BITCODE = NO; 442 | INFOPLIST_FILE = Runner/Info.plist; 443 | LD_RUNPATH_SEARCH_PATHS = ( 444 | "$(inherited)", 445 | "@executable_path/Frameworks", 446 | ); 447 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 448 | PRODUCT_NAME = "$(TARGET_NAME)"; 449 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 450 | SWIFT_VERSION = 5.0; 451 | VERSIONING_SYSTEM = "apple-generic"; 452 | }; 453 | name = Release; 454 | }; 455 | /* End XCBuildConfiguration section */ 456 | 457 | /* Begin XCConfigurationList section */ 458 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 459 | isa = XCConfigurationList; 460 | buildConfigurations = ( 461 | 97C147031CF9000F007C117D /* Debug */, 462 | 97C147041CF9000F007C117D /* Release */, 463 | 249021D3217E4FDB00AE95B9 /* Profile */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Release; 467 | }; 468 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 469 | isa = XCConfigurationList; 470 | buildConfigurations = ( 471 | 97C147061CF9000F007C117D /* Debug */, 472 | 97C147071CF9000F007C117D /* Release */, 473 | 249021D4217E4FDB00AE95B9 /* Profile */, 474 | ); 475 | defaultConfigurationIsVisible = 0; 476 | defaultConfigurationName = Release; 477 | }; 478 | /* End XCConfigurationList section */ 479 | }; 480 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 481 | } 482 | --------------------------------------------------------------------------------