diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c7f0c06 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,10 @@ +github: letsar +patreon: romainrastel +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: ['https://www.buymeacoffee.com/romainrastel', 'paypal.me/RomainRastel'] diff --git a/CHANGELOG.md b/CHANGELOG.md index ad5ef16..2034efc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,62 @@ +## 0.6.4 +### Fixed +* Not passing right overlap contraints to sliver child. Now we can create nested sticky headers! + +## 0.6.3 +### Fixed +* Hit Test on not sticky header + +## 0.6.2 +### Fixed +* Hit Test on not sticky header + +## 0.6.1 +### Fixed +* Error due to null-safety migration. + +## 0.6.0 +### Changed +* Migrated to sound null-safety. +* Increase the minimum version of Flutter. +* Increase the minimum version of Dart SDK. + +## 0.5.0 +### Changed +* The minimum version of Flutter. + +## 0.4.6 +### Added +* A new SliverStickyHeader.builder constructor instead of the deprecated SliverStickyHeaderBuilder. +* A dependency to value_layout_builder in order to manage the SliverStickyHeader.builder. + +### Removed +* Custom code to make SliverStickyHeader.builder work. + +## 0.4.5 +### Fixed +* Null references issues in debug mode. + +## 0.4.4 +### Fixed +* Static analysis issues. + +## 0.4.3 +### Fixed +* Static analysis issues. + +## 0.4.2 +### Added +* A StickyHeaderController to get the scroll offset of the current sticky header. + +## 0.4.1 +### Added +* A sticky parameter to specify whether the header is sticky or not. + +## 0.4.0 + +* Updated SDK constraint to support new error message formats. +* Updated error message formats + ## 0.3.4 ### Removed * Print call for headerPosition diff --git a/README.md b/README.md index 3be2638..240a9f2 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ A Flutter implementation of sticky headers with a sliver as a child. * Notifies when the header scrolls outside the viewport. * Can scroll in any direction. * Supports overlapping (AppBars for example). +* Supports not sticky headers (with `sticky: false` parameter). +* Supports a controller which notifies the scroll offset of the current sticky header. ## Getting started @@ -22,7 +24,7 @@ In the `pubspec.yaml` of your flutter project, add the following dependency: ```yaml dependencies: ... - flutter_sticky_header: "^0.3.4" + flutter_sticky_header: ``` In your library add the following import: @@ -38,24 +40,24 @@ For help getting started with Flutter, view the online [documentation](https://f You can place one or multiple `SliverStickyHeader`s inside a `CustomScrollView`. ```dart -new SliverStickyHeader( - header: new Container( +SliverStickyHeader( + header: Container( height: 60.0, color: Colors.lightBlue, padding: EdgeInsets.symmetric(horizontal: 16.0), alignment: Alignment.centerLeft, - child: new Text( + child: Text( 'Header #0', style: const TextStyle(color: Colors.white), ), ), - sliver: new SliverList( - delegate: new SliverChildBuilderDelegate( - (context, i) => new ListTile( - leading: new CircleAvatar( - child: new Text('0'), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('0'), ), - title: new Text('List tile #$i'), + title: Text('List tile #$i'), ), childCount: 4, ), @@ -63,32 +65,32 @@ new SliverStickyHeader( ); ``` -## SliverStickyHeaderBuilder +## SliverStickyHeader.builder -If you want to change the header layout during its scroll, you can use the `SliverStickyHeaderBuilder`. +If you want to change the header layout during its scroll, you can use the `SliverStickyHeader.builder` constructor. The example belows changes the opacity of the header as it scrolls off the viewport. ```dart -new SliverStickyHeaderBuilder( - builder: (context, state) => new Container( +SliverStickyHeader.builder( + builder: (context, state) => Container( height: 60.0, color: (state.isPinned ? Colors.pink : Colors.lightBlue) .withOpacity(1.0 - state.scrollPercentage), padding: EdgeInsets.symmetric(horizontal: 16.0), alignment: Alignment.centerLeft, - child: new Text( + child: Text( 'Header #1', style: const TextStyle(color: Colors.white), ), ), - sliver: new SliverList( - delegate: new SliverChildBuilderDelegate( - (context, i) => new ListTile( - leading: new CircleAvatar( - child: new Text('0'), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('0'), ), - title: new Text('List tile #$i'), + title: Text('List tile #$i'), ), childCount: 4, ), @@ -98,6 +100,10 @@ new SliverStickyHeaderBuilder( You can find more examples in the [Example](https://github.com/letsar/flutter_sticky_header/tree/master/example) project. +## Sponsoring + +I'm working on my packages on my free-time, but I don't have as much time as I would. If this package or any other package I created is helping you, please consider to sponsor me. By doing so, I will prioritize your issues or your pull-requests before the others. + ## Changelog Please see the [Changelog](https://github.com/letsar/flutter_sticky_header/blob/master/CHANGELOG.md) page to know what's recently changed. diff --git a/example/.gitignore b/example/.gitignore index dee655c..f3c2053 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -1,9 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp .DS_Store -.dart_tool/ +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies .packages +.pub-cache/ .pub/ +/build/ -build/ +# Web related +lib/generated_plugin_registrant.dart -.flutter-plugins +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/example/.metadata b/example/.metadata index 94ee986..bcef94e 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,5 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: c7ea3ca377e909469c68f2ab878a5bc53d3cf66b - channel: dev + revision: bbfbf1770cca2da7c82e887e4e4af910034800b6 + channel: stable + +project_type: app diff --git a/example/README.md b/example/README.md index 64a12f6..a135626 100644 --- a/example/README.md +++ b/example/README.md @@ -4,5 +4,13 @@ A new Flutter project. ## Getting Started -For help getting started with Flutter, view our online -[documentation](https://flutter.io/). +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example/android/.gitignore b/example/android/.gitignore index 65b7315..6f56801 100644 --- a/example/android/.gitignore +++ b/example/android/.gitignore @@ -1,10 +1,13 @@ -*.iml -*.class -.gradle +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat /local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index a23f7c0..ce52202 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -11,24 +11,43 @@ if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 27 + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } - lintOptions { - disable 'InvalidPackage' + sourceSets { + main.java.srcDirs += 'src/main/kotlin' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.letsar.fsh.example.example" - minSdkVersion 16 - targetSdkVersion 27 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + applicationId "com.letsar.sh.example" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName } buildTypes { @@ -45,7 +64,5 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.1' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..6ea83aa --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 37fde23..ef91f0f 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,39 +1,34 @@ - - - - - - + - + + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> + + diff --git a/example/android/app/src/main/java/com/letsar/fsh/example/example/MainActivity.java b/example/android/app/src/main/java/com/letsar/fsh/example/example/MainActivity.java deleted file mode 100644 index 8672db0..0000000 --- a/example/android/app/src/main/java/com/letsar/fsh/example/example/MainActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.letsar.fsh.example.example; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class MainActivity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/example/android/app/src/main/kotlin/com/letsar/sh/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/letsar/sh/example/MainActivity.kt new file mode 100644 index 0000000..e4b5f8e --- /dev/null +++ b/example/android/app/src/main/kotlin/com/letsar/sh/example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.letsar.sh.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..3db14bb --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml index 00fa441..d460d1e 100644 --- a/example/android/app/src/main/res/values/styles.xml +++ b/example/android/app/src/main/res/values/styles.xml @@ -1,8 +1,18 @@ - + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..6ea83aa --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle index 4476887..4256f91 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,18 +1,20 @@ buildscript { + ext.kotlin_version = '1.6.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 8bd86f6..94adc3a 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1 +1,3 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index aa901e1..bc6a58a 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/example/android/gradlew b/example/android/gradlew old mode 100644 new mode 100755 diff --git a/example/android/gradlew.bat b/example/android/gradlew.bat index 8a0b282..aec9973 100644 --- a/example/android/gradlew.bat +++ b/example/android/gradlew.bat @@ -1,90 +1,90 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 5a2f14f..44e62bc 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,15 +1,11 @@ include ':app' -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/ios/.gitignore b/example/ios/.gitignore index 79cc4da..e96ef60 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -1,45 +1,32 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser *.mode1v3 *.mode2v3 +*.moved-aside +*.pbxuser *.perspectivev3 - -!default.pbxuser +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. !default.mode1v3 !default.mode2v3 +!default.pbxuser !default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/app.flx -/Flutter/app.zip -/Flutter/flutter_assets/ -/Flutter/App.framework -/Flutter/Flutter.framework -/Flutter/Generated.xcconfig -/ServiceDefinitions.json - -Pods/ -.symlinks/ diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9367d48..f2872cf 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable App CFBundleIdentifier @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 623d510..2109967 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,21 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -30,8 +22,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -41,17 +31,13 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -63,8 +49,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -74,10 +58,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -91,7 +72,6 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, ); sourceTree = ""; }; @@ -106,27 +86,18 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -156,17 +127,18 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0910; - ORGANIZATIONNAME = "The Chromium Authors"; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -188,11 +160,8 @@ buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -212,7 +181,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -235,8 +204,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -263,9 +231,84 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.letsar.sh.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -277,12 +320,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -309,7 +354,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -319,7 +364,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -331,12 +375,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -357,9 +403,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -370,7 +418,8 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -382,8 +431,11 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.letsar.fsh.example.example; + PRODUCT_BUNDLE_IDENTIFIER = com.letsar.sh.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -393,7 +445,8 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -405,8 +458,10 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.letsar.fsh.example.example; + PRODUCT_BUNDLE_IDENTIFIER = com.letsar.sh.example; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -419,6 +474,7 @@ buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -428,6 +484,7 @@ buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a1..919434a 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1263ac8..3db53b6 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ @@ -46,7 +45,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -67,7 +65,7 @@ + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bb..0000000 --- a/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e9..0000000 --- a/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 3d43d11..dc9ada4 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 90181b7..1579fb3 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,11 +15,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion - 1 + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName @@ -41,5 +41,7 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/Runner/main.m b/example/ios/Runner/main.m deleted file mode 100644 index dff6597..0000000 --- a/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/example/lib/common.dart b/example/lib/common.dart new file mode 100644 index 0000000..60c52e8 --- /dev/null +++ b/example/lib/common.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +class AppScaffold extends StatelessWidget { + const AppScaffold({ + Key? key, + required this.title, + required this.slivers, + this.reverse = false, + }) : super(key: key); + + final String title; + final List slivers; + final bool reverse; + + @override + Widget build(BuildContext context) { + return DefaultStickyHeaderController( + child: Scaffold( + appBar: AppBar( + title: Text(title), + ), + body: CustomScrollView( + slivers: slivers, + reverse: reverse, + ), + floatingActionButton: const _FloatingActionButton(), + ), + ); + } +} + +class _FloatingActionButton extends StatelessWidget { + const _FloatingActionButton({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FloatingActionButton( + child: Icon(Icons.adjust), + backgroundColor: Colors.green, + onPressed: () { + final double offset = + DefaultStickyHeaderController.of(context)!.stickyHeaderScrollOffset; + PrimaryScrollController.of(context)!.animateTo( + offset, + duration: Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + }, + ); + } +} + +class Header extends StatelessWidget { + const Header({ + Key? key, + this.index, + this.title, + this.color = Colors.lightBlue, + }) : super(key: key); + + final String? title; + final int? index; + final Color color; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + print('hit $index'); + }, + child: Container( + height: 60, + color: color, + padding: EdgeInsets.symmetric(horizontal: 16.0), + alignment: Alignment.centerLeft, + child: Text( + title ?? 'Header #$index', + style: const TextStyle(color: Colors.white), + ), + ), + ); + } +} diff --git a/example/lib/examples/animated_header.dart b/example/lib/examples/animated_header.dart new file mode 100644 index 0000000..f624c57 --- /dev/null +++ b/example/lib/examples/animated_header.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class AnimatedHeaderExample extends StatelessWidget { + const AnimatedHeaderExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: 'Animated header Example', + slivers: [ + _StickyHeaderList(index: 0), + _StickyHeaderList(index: 1), + _StickyHeaderList(index: 2), + _StickyHeaderList(index: 3), + ], + ); + } +} + +class _StickyHeaderList extends StatelessWidget { + const _StickyHeaderList({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader.builder( + builder: (context, state) => _AnimatedHeader( + state: state, + index: index, + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ); + } +} + +class _AnimatedHeader extends StatelessWidget { + const _AnimatedHeader({ + Key? key, + this.state, + this.index, + }) : super(key: key); + + final int? index; + final SliverStickyHeaderState? state; + + @override + Widget build(BuildContext context) { + return Header( + index: index, + color: Colors.lightBlue.withOpacity(1 - state!.scrollPercentage), + ); + } +} diff --git a/example/lib/examples/grid.dart b/example/lib/examples/grid.dart new file mode 100644 index 0000000..da1af75 --- /dev/null +++ b/example/lib/examples/grid.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class GridExample extends StatelessWidget { + const GridExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: 'Grid Example', + slivers: [ + _StickyHeaderGrid(index: 0), + _StickyHeaderGrid(index: 1), + _StickyHeaderGrid(index: 2), + _StickyHeaderGrid(index: 3), + ], + ); + } +} + +class _StickyHeaderGrid extends StatelessWidget { + const _StickyHeaderGrid({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + header: Header(index: index), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, crossAxisSpacing: 4.0, mainAxisSpacing: 4.0), + delegate: SliverChildBuilderDelegate( + (context, i) => GridTile( + child: Card( + child: Container( + color: Colors.green, + ), + ), + footer: Container( + color: Colors.white.withOpacity(0.5), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Grid tile #$i', + style: const TextStyle(color: Colors.black), + ), + ), + ), + ), + childCount: 9, + ), + ), + ); + } +} diff --git a/example/lib/examples/list.dart b/example/lib/examples/list.dart new file mode 100644 index 0000000..f127bac --- /dev/null +++ b/example/lib/examples/list.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class ListExample extends StatelessWidget { + const ListExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: 'List Example', + slivers: [ + _StickyHeaderList(index: 0), + _StickyHeaderList(index: 1), + _StickyHeaderList(index: 2), + _StickyHeaderList(index: 3), + ], + ); + } +} + +class _StickyHeaderList extends StatelessWidget { + const _StickyHeaderList({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + header: Header(index: index), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + onTap: () { + print('tile $i'); + }, + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ); + } +} diff --git a/example/lib/examples/mix_slivers.dart b/example/lib/examples/mix_slivers.dart new file mode 100644 index 0000000..ce4b8e3 --- /dev/null +++ b/example/lib/examples/mix_slivers.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class MixSliversExample extends StatelessWidget { + const MixSliversExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: 'List Example', + slivers: [ + SliverAppBar( + backgroundColor: Colors.orange, + title: Text('SliverAppBar'), + automaticallyImplyLeading: false, + pinned: true, + ), + SliverToBoxAdapter( + child: Container( + height: 50, + color: Colors.red, + ), + ), + _StickyHeaderList(index: 0), + _StickyHeaderList(index: 1), + _StickyHeaderList(index: 2), + _StickyHeaderList(index: 3), + ], + ); + } +} + +class _StickyHeaderList extends StatelessWidget { + const _StickyHeaderList({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + header: Header(index: index), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ); + } +} diff --git a/example/lib/examples/nested.dart b/example/lib/examples/nested.dart new file mode 100644 index 0000000..4affc58 --- /dev/null +++ b/example/lib/examples/nested.dart @@ -0,0 +1,124 @@ +import 'package:example/common.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:sliver_tools/sliver_tools.dart'; + +class NestedExample extends StatelessWidget { + const NestedExample({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: 'Nested Sticky Headers', + slivers: [ + SliverStickyHeader( + header: Header(title: '1'), + sliver: MultiSliver( + children: [ + SliverStickyHeader( + header: Header(title: '1.1'), + sliver: _SliverLeaf(), + ), + SliverStickyHeader( + header: Header(title: '1.2'), + sliver: MultiSliver( + children: [ + SliverStickyHeader( + header: Header(title: '1.2.1'), + sliver: _SliverLeaf(), + ), + SliverStickyHeader( + header: Header(title: '1.2.2'), + sliver: _SliverLeaf(), + ), + SliverStickyHeader( + header: Header(title: '1.2.3'), + sliver: _SliverLeaf(), + ), + ], + ), + ), + SliverStickyHeader( + header: Header(title: '1.3'), + sliver: _SliverLeaf(), + ), + ], + ), + ), + SliverStickyHeader( + header: Header(title: '2'), + sliver: MultiSliver( + children: [ + SliverStickyHeader( + header: Header(title: '2.1'), + sliver: _SliverLeaf(), + ), + SliverStickyHeader( + header: Header(title: '2.2'), + sliver: MultiSliver( + children: [ + SliverStickyHeader( + header: Header(title: '2.2.1'), + sliver: _SliverLeaf(), + ), + SliverStickyHeader( + header: Header(title: '2.2.2'), + sliver: _SliverLeaf(), + ), + SliverStickyHeader( + header: Header(title: '2.2.3'), + sliver: _SliverLeaf(), + ), + ], + ), + ), + SliverStickyHeader( + header: Header(title: '2.3'), + sliver: _SliverLeaf(), + ), + ], + ), + ), + SliverStickyHeader( + header: Header(title: '3'), + sliver: _SliverLeaf(), + ), + SliverStickyHeader( + header: Header(title: '4'), + sliver: MultiSliver( + children: [ + SliverStickyHeader( + header: Header(title: '4.1'), + sliver: _SliverLeaf(), + ), + SliverStickyHeader( + header: Header(title: '4.2'), + sliver: _SliverLeaf(), + ), + SliverStickyHeader( + header: Header(title: '4.3'), + sliver: _SliverLeaf(), + ), + ], + ), + ), + ], + ); + } +} + +class _SliverLeaf extends StatelessWidget { + const _SliverLeaf(); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Container( + height: 200, + color: Colors.amber, + ), + ); + } +} diff --git a/example/lib/examples/not_sticky.dart b/example/lib/examples/not_sticky.dart new file mode 100644 index 0000000..3ad8405 --- /dev/null +++ b/example/lib/examples/not_sticky.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class NotStickyExample extends StatelessWidget { + const NotStickyExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: 'Not Sticky Example', + slivers: [ + _NotStickyList(index: 0), + _NotStickyList(index: 1), + _NotStickyList(index: 2), + _NotStickyList(index: 3), + ], + ); + } +} + +class _NotStickyList extends StatelessWidget { + const _NotStickyList({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + header: Header(index: index), + sticky: false, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + onTap: () { + print('tile $i'); + }, + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ); + } +} diff --git a/example/lib/examples/reverse.dart b/example/lib/examples/reverse.dart new file mode 100644 index 0000000..17fea56 --- /dev/null +++ b/example/lib/examples/reverse.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class ReverseExample extends StatelessWidget { + const ReverseExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppScaffold( + reverse: true, + title: 'Reverse Example', + slivers: [ + _StickyHeaderList(index: 0), + _StickyHeaderList(index: 1), + _StickyHeaderList(index: 2), + _StickyHeaderList(index: 3), + ], + ); + } +} + +class _StickyHeaderList extends StatelessWidget { + const _StickyHeaderList({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + header: Header(index: index), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ); + } +} diff --git a/example/lib/examples/reverse2.dart b/example/lib/examples/reverse2.dart new file mode 100644 index 0000000..ee0f73e --- /dev/null +++ b/example/lib/examples/reverse2.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class ReverseExample2 extends StatelessWidget { + const ReverseExample2({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppScaffold( + reverse: true, + title: 'Reverse Example', + slivers: [ + _StickyHeaderList(index: 0), + _StickyHeaderList(index: 1), + _StickyHeaderList(index: 2), + _StickyHeaderList(index: 3), + ], + ); + } +} + +class _StickyHeaderList extends StatelessWidget { + const _StickyHeaderList({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + header: Header(index: index), + reverse: true, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ); + } +} diff --git a/example/lib/examples/side_header.dart b/example/lib/examples/side_header.dart new file mode 100644 index 0000000..a7602da --- /dev/null +++ b/example/lib/examples/side_header.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class SideHeaderExample extends StatelessWidget { + const SideHeaderExample({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: 'Side Header Example', + slivers: [ + _StickyHeaderGrid(index: 0), + _StickyHeaderGrid(index: 1), + _StickyHeaderGrid(index: 2), + _StickyHeaderGrid(index: 3), + ], + ); + } +} + +class _StickyHeaderGrid extends StatelessWidget { + const _StickyHeaderGrid({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + overlapsContent: true, + header: _SideHeader(index: index), + sliver: SliverPadding( + padding: const EdgeInsets.only(left: 60), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, crossAxisSpacing: 4.0, mainAxisSpacing: 4.0), + delegate: SliverChildBuilderDelegate( + (context, i) => GridTile( + child: Card( + child: Container( + color: Colors.green, + ), + ), + footer: Container( + color: Colors.white.withOpacity(0.5), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Grid tile #$i', + style: const TextStyle(color: Colors.black), + ), + ), + ), + ), + childCount: 9, + ), + ), + ), + ); + } +} + +class _SideHeader extends StatelessWidget { + const _SideHeader({ + Key? key, + this.index, + }) : super(key: key); + + final int? index; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Align( + alignment: Alignment.centerLeft, + child: SizedBox( + height: 44.0, + width: 44.0, + child: CircleAvatar( + backgroundColor: Colors.orangeAccent, + foregroundColor: Colors.white, + child: Text('$index'), + ), + ), + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index dc06166..bc624db 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,310 +1,115 @@ +import 'package:example/examples/nested.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sticky_header/flutter_sticky_header.dart'; -void main() => runApp(new MyApp()); +import 'examples/animated_header.dart'; +import 'examples/grid.dart'; +import 'examples/list.dart'; +import 'examples/mix_slivers.dart'; +import 'examples/not_sticky.dart'; +import 'examples/reverse.dart'; +import 'examples/reverse2.dart'; +import 'examples/side_header.dart'; + +void main() { + // debugPaintPointersEnabled = true; + runApp(const App()); +} + +class App extends StatelessWidget { + const App({ + Key? key, + }) : super(key: key); -class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return new MaterialApp( - title: 'Sticky Header example', - theme: new ThemeData( - primarySwatch: Colors.blue, - ), - home: new MainScreen(), + return MaterialApp( + title: 'Flutter Sticky Headers', + home: const _Home(), ); } } -class MainScreen extends StatelessWidget { +class _Home extends StatelessWidget { + const _Home({ + Key? key, + }) : super(key: key); + @override Widget build(BuildContext context) { - return new SimpleScaffold( - title: 'Flutter Sticky Header example', - child: new Builder(builder: (BuildContext context) { - return new CustomScrollView( - slivers: _buildSlivers(context), - ); - }), - ); - } - - List _buildSlivers(BuildContext context) { - List slivers = new List(); - - //slivers.add(_buildExample()); - //slivers.add(_buildBuilderExample()); - int i = 0; - slivers.add(SliverAppBar( - backgroundColor: Colors.blue.withOpacity(0.5), - title: Text('text'), - pinned: true, - )); - slivers.add(SliverAppBar( - backgroundColor: Colors.yellow.withOpacity(0.5), - title: Text('text'), - pinned: true, - )); - slivers.addAll(_buildHeaderBuilderLists(context, i, i += 5)); - slivers.addAll(_buildLists(context, i, i += 3)); - slivers.addAll(_buildGrids(context, i, i += 3)); - slivers.addAll(_buildSideHeaderGrids(context, i, i += 3)); - slivers.addAll(_buildHeaderBuilderLists(context, i, i += 5)); - return slivers; - } - - List _buildLists(BuildContext context, int firstIndex, int count) { - return List.generate(count, (sliverIndex) { - sliverIndex += firstIndex; - return new SliverStickyHeader( - header: _buildHeader(sliverIndex), - sliver: new SliverList( - delegate: new SliverChildBuilderDelegate( - (context, i) => new ListTile( - leading: new CircleAvatar( - child: new Text('$sliverIndex'), - ), - title: new Text('List tile #$i'), - ), - childCount: 4, + return Scaffold( + appBar: AppBar( + title: Text('Flutter Sticky Headers'), + ), + body: ListView( + children: [ + _Item( + text: 'List Example', + builder: (_) => const ListExample(), ), - ), - ); - }); - } - - List _buildHeaderBuilderLists( - BuildContext context, int firstIndex, int count) { - return List.generate(count, (sliverIndex) { - sliverIndex += firstIndex; - return new SliverStickyHeaderBuilder( - builder: (context, state) => - _buildAnimatedHeader(context, sliverIndex, state), - sliver: new SliverList( - delegate: new SliverChildBuilderDelegate( - (context, i) => new ListTile( - leading: new CircleAvatar( - child: new Text('$sliverIndex'), - ), - title: new Text('List tile #$i'), - ), - childCount: 4, + _Item( + text: 'Grid Example', + builder: (_) => const GridExample(), ), - ), - ); - }); - } - - List _buildGrids(BuildContext context, int firstIndex, int count) { - return List.generate(count, (sliverIndex) { - sliverIndex += firstIndex; - return new SliverStickyHeader( - header: _buildHeader(sliverIndex), - sliver: new SliverGrid( - gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, crossAxisSpacing: 4.0, mainAxisSpacing: 4.0), - delegate: new SliverChildBuilderDelegate( - (context, i) => GestureDetector( - onTap: () => Scaffold.of(context).showSnackBar( - new SnackBar(content: Text('Grid tile #$i'))), - child: new GridTile( - child: Card( - child: new Container( - color: Colors.green, - ), - ), - footer: new Container( - color: Colors.white.withOpacity(0.5), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: new Text( - 'Grid tile #$i', - style: const TextStyle(color: Colors.black), - ), - ), - ), - ), - ), - childCount: 9, + _Item( + text: 'Not Sticky Example', + builder: (_) => const NotStickyExample(), ), - ), - ); - }); - } - - List _buildSideHeaderGrids( - BuildContext context, int firstIndex, int count) { - return List.generate(count, (sliverIndex) { - sliverIndex += firstIndex; - return new SliverStickyHeader( - overlapsContent: true, - header: _buildSideHeader(context, sliverIndex), - sliver: new SliverPadding( - padding: new EdgeInsets.only(left: 60.0), - sliver: new SliverGrid( - gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - crossAxisSpacing: 4.0, - mainAxisSpacing: 4.0, - childAspectRatio: 1.0), - delegate: new SliverChildBuilderDelegate( - (context, i) => GestureDetector( - onTap: () => Scaffold.of(context).showSnackBar( - new SnackBar(content: Text('Grid tile #$i'))), - child: new GridTile( - child: Card( - child: new Container( - color: Colors.orange, - ), - ), - footer: new Container( - color: Colors.white.withOpacity(0.5), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: new Text( - 'Grid tile #$i', - style: const TextStyle(color: Colors.black), - ), - ), - ), - ), - ), - childCount: 12, - ), + _Item( + text: 'Side Header Example', + builder: (_) => const SideHeaderExample(), ), - ), - ); - }); - } - - Widget _buildHeader(int index, {String text}) { - return new Container( - height: 60.0, - color: Colors.lightBlue, - padding: EdgeInsets.symmetric(horizontal: 16.0), - alignment: Alignment.centerLeft, - child: new Text( - text ?? 'Header #$index', - style: const TextStyle(color: Colors.white), - ), - ); - } - - Widget _buildSideHeader(BuildContext context, int index, {String text}) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Align( - alignment: Alignment.centerLeft, - child: new SizedBox( - height: 44.0, - width: 44.0, - child: GestureDetector( - onTap: () => Scaffold - .of(context) - .showSnackBar(new SnackBar(content: Text('$index'))), - child: new CircleAvatar( - backgroundColor: Colors.orangeAccent, - foregroundColor: Colors.white, - child: new Text('$index'), - ), + _Item( + text: 'Animated Header Example', + builder: (_) => const AnimatedHeaderExample(), ), - ), - ), - ); - } - - Widget _buildAnimatedHeader( - BuildContext context, int index, SliverStickyHeaderState state) { - return GestureDetector( - onTap: () => Scaffold - .of(context) - .showSnackBar(new SnackBar(content: Text('$index'))), - child: new Container( - height: 60.0, - color: (state.isPinned ? Colors.pink : Colors.lightBlue) - .withOpacity(1.0 - state.scrollPercentage), - padding: EdgeInsets.symmetric(horizontal: 16.0), - alignment: Alignment.centerLeft, - child: new Text( - 'Header #$index', - style: const TextStyle(color: Colors.white), - ), - ), - ); - } - - Widget _buildExample() { - return new SliverStickyHeader( - header: new Container( - height: 60.0, - color: Colors.lightBlue, - padding: EdgeInsets.symmetric(horizontal: 16.0), - alignment: Alignment.centerLeft, - child: new Text( - 'Header #0', - style: const TextStyle(color: Colors.white), - ), - ), - sliver: new SliverList( - delegate: new SliverChildBuilderDelegate( - (context, i) => new ListTile( - leading: new CircleAvatar( - child: new Text('0'), - ), - title: new Text('List tile #$i'), - ), - childCount: 4, - ), - ), - ); - } - - Widget _buildBuilderExample() { - return new SliverStickyHeaderBuilder( - builder: (context, state) => new Container( - height: 60.0, - color: (state.isPinned ? Colors.pink : Colors.lightBlue) - .withOpacity(1.0 - state.scrollPercentage), - padding: EdgeInsets.symmetric(horizontal: 16.0), - alignment: Alignment.centerLeft, - child: new Text( - 'Header #1', - style: const TextStyle(color: Colors.white), - ), + _Item( + text: 'Reverse List Example', + builder: (_) => const ReverseExample(), ), - sliver: new SliverList( - delegate: new SliverChildBuilderDelegate( - (context, i) => new ListTile( - leading: new CircleAvatar( - child: new Text('0'), - ), - title: new Text('List tile #$i'), - ), - childCount: 4, - ), + _Item( + text: 'Reverse List Example 2', + builder: (_) => const ReverseExample2(), + ), + _Item( + text: 'Mixing other slivers', + builder: (_) => const MixSliversExample(), + ), + _Item( + text: 'Nested sticky headers', + builder: (_) => const NestedExample(), + ), + ], ), ); } } -class SimpleScaffold extends StatelessWidget { - const SimpleScaffold({ - Key key, - this.title, - this.child, +class _Item extends StatelessWidget { + const _Item({ + Key? key, + required this.text, + required this.builder, }) : super(key: key); - final String title; - - final Widget child; + final String text; + final WidgetBuilder builder; @override Widget build(BuildContext context) { - return new Scaffold( - appBar: new AppBar( - title: new Text(title), + return Card( + color: Colors.blue, + child: InkWell( + onTap: () => + Navigator.push(context, MaterialPageRoute(builder: builder)), + child: Container( + padding: EdgeInsets.all(16), + child: Text( + text, + style: TextStyle( + color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20), + ), + ), ), - body: child, ); } } diff --git a/example/lib/main_issue_75.dart b/example/lib/main_issue_75.dart new file mode 100644 index 0000000..a30f1cd --- /dev/null +++ b/example/lib/main_issue_75.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +void main() { + runApp(MaterialApp(home: TabHeaderExample())); +} + +class TabHeaderExample extends StatefulWidget { + const TabHeaderExample({Key? key}) : super(key: key); + + @override + State createState() => _TabHeaderExampleState(); +} + +class _TabHeaderExampleState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Tab List Example'), + ), + body: CustomScrollView( + slivers: [ + _StickyHeaderList(index: 0), + _StickyHeaderList(index: 1), + _StickyHeaderList(index: 2), + _StickyHeaderList(index: 3), + ], + ), + ); + } +} + +class _StickyHeaderList extends StatefulWidget { + final int index; + const _StickyHeaderList({Key? key, required this.index}) : super(key: key); + + @override + State<_StickyHeaderList> createState() => _StickyHeaderListState(); +} + +class _StickyHeaderListState extends State<_StickyHeaderList> + with TickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 10, vsync: this); + } + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + header: Container( + color: Colors.white, + height: 47, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TabBar( + isScrollable: true, + controller: _tabController, + indicatorColor: const Color(0xFF216DDF), + indicatorSize: TabBarIndicatorSize.label, + indicatorWeight: 2, + indicatorPadding: EdgeInsets.only(bottom: 10), + unselectedLabelColor: const Color(0xFF888888), + unselectedLabelStyle: TextStyle(fontSize: 17), + labelColor: const Color(0xFF216DDF), + labelStyle: + TextStyle(fontSize: 17, fontWeight: FontWeight.bold), + tabs: _createTabs(), + onTap: (index) { + print(index); + }, + ), + ), + InkWell( + child: Container( + decoration: BoxDecoration( + color: Colors.red, + boxShadow: [ + BoxShadow( + color: Colors.white, + blurRadius: 8, + offset: Offset(-20, 0), + ), + ], + ), + padding: EdgeInsets.only(left: 5, right: 12), + child: Row( + children: [ + Text( + "全部", + style: TextStyle( + color: const Color(0xFF101010), fontSize: 17), + ), + ], + ), + ), + onTap: () { + // Can not response + print("1234567"); + }, + ), + ], + ), + ), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + onTap: () { + print('tile $i'); + }, + leading: CircleAvatar( + child: Text('${widget.index}'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ); + } + + /// 创建Tabs + List _createTabs() { + return [ + "我是tab", + "我是tab", + "我是tab", + "我是tab", + "我是tab", + "我是tab", + "我是tab", + "我是tab", + "我是tab", + "我是tab" + ].map((e) => Tab(text: e)).toList(); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b2da0ad..903f267 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,24 +1,43 @@ name: example description: A new Flutter project. +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: '>=2.17.0 <3.0.0' + dependencies: flutter: sdk: flutter + flutter_sticky_header: + path: ../ + # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 - - flutter_sticky_header: - path: ../ + cupertino_icons: ^1.0.2 + sliver_tools: ^0.2.7 dev_dependencies: flutter_test: sdk: flutter - # For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec +# following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: @@ -30,14 +49,14 @@ flutter: # To add assets to your application, add an assets section, like this: # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. + # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages + # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a @@ -57,4 +76,4 @@ flutter: # weight: 700 # # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/lib/src/rendering/sliver_sticky_header.dart b/lib/src/rendering/sliver_sticky_header.dart index e7d871c..3ff58e4 100644 --- a/lib/src/rendering/sliver_sticky_header.dart +++ b/lib/src/rendering/sliver_sticky_header.dart @@ -1,10 +1,10 @@ import 'dart:math' as math; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_sticky_header/flutter_sticky_header.dart'; -import 'package:flutter_sticky_header/src/rendering/sticky_header_constraints.dart'; -import 'package:flutter_sticky_header/src/rendering/sticky_header_layout_builder.dart'; +// import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import '../../flutter_sticky_header.dart'; +import 'package:value_layout_builder/value_layout_builder.dart'; /// A sliver with a [RenderBox] as header and a [RenderSliver] as child. /// @@ -12,104 +12,149 @@ import 'package:flutter_sticky_header/src/rendering/sticky_header_layout_builder /// the [child] scrolls off the viewport. class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { RenderSliverStickyHeader({ - RenderObject header, - RenderSliver child, - overlapsContent: false, - }) : assert(overlapsContent != null), - _overlapsContent = overlapsContent { - this.header = header; + RenderObject? header, + RenderSliver? child, + bool overlapsContent: false, + bool reverse: false, + bool sticky: true, + StickyHeaderController? controller, + }) : _overlapsContent = overlapsContent, + _sticky = sticky, + _controller = controller, + _reverse = reverse + { + this.header = header as RenderBox?; this.child = child; } - SliverStickyHeaderState _oldState; - double _headerExtent; - bool _isPinned; + SliverStickyHeaderState? _oldState; + double? _headerExtent; + late bool _isPinned; bool get overlapsContent => _overlapsContent; bool _overlapsContent; + set overlapsContent(bool value) { - assert(value != null); if (_overlapsContent == value) return; _overlapsContent = value; markNeedsLayout(); } + bool get reverse => _reverse; + bool _reverse; + + set reverse(bool value) { + assert(value != null); + if (_reverse == value) return; + _reverse = value; + markNeedsLayout(); + } + + bool get sticky => _sticky; + bool _sticky; + + set sticky(bool value) { + if (_sticky == value) return; + _sticky = value; + markNeedsLayout(); + } + + StickyHeaderController? get controller => _controller; + StickyHeaderController? _controller; + + set controller(StickyHeaderController? value) { + if (_controller == value) return; + if (_controller != null && value != null) { + // We copy the state of the old controller. + value.stickyHeaderScrollOffset = _controller!.stickyHeaderScrollOffset; + } + _controller = value; + } + /// The render object's header - RenderBox get header => _header; - RenderBox _header; - set header(RenderBox value) { - if (_header != null) dropChild(_header); + RenderBox? get header => _header; + RenderBox? _header; + + set header(RenderBox? value) { + if (_header != null) dropChild(_header!); _header = value; - if (_header != null) adoptChild(_header); + if (_header != null) adoptChild(_header!); } /// The render object's unique child - RenderSliver get child => _child; - RenderSliver _child; - set child(RenderSliver value) { - if (_child != null) dropChild(_child); + RenderSliver? get child => _child; + RenderSliver? _child; + + set child(RenderSliver? value) { + if (_child != null) dropChild(_child!); _child = value; - if (_child != null) adoptChild(_child); + if (_child != null) adoptChild(_child!); } @override void setupParentData(RenderObject child) { if (child.parentData is! SliverPhysicalParentData) - child.parentData = new SliverPhysicalParentData(); + child.parentData = SliverPhysicalParentData(); } @override void attach(PipelineOwner owner) { super.attach(owner); - if (_header != null) _header.attach(owner); - if (_child != null) _child.attach(owner); + if (_header != null) _header!.attach(owner); + if (_child != null) _child!.attach(owner); } @override void detach() { super.detach(); - if (_header != null) _header.detach(); - if (_child != null) _child.detach(); + if (_header != null) _header!.detach(); + if (_child != null) _child!.detach(); } @override void redepthChildren() { - if (_header != null) redepthChild(_header); - if (_child != null) redepthChild(_child); + if (_header != null) redepthChild(_header!); + if (_child != null) redepthChild(_child!); } @override void visitChildren(RenderObjectVisitor visitor) { - if (_header != null) visitor(_header); - if (_child != null) visitor(_child); + if (_header != null) visitor(_header!); + if (_child != null) visitor(_child!); } @override List debugDescribeChildren() { List result = []; if (header != null) { - result.add(header.toDiagnosticsNode(name: 'header')); + result.add(header!.toDiagnosticsNode(name: 'header')); } if (child != null) { - result.add(child.toDiagnosticsNode(name: 'child')); + result.add(child!.toDiagnosticsNode(name: 'child')); } return result; } double computeHeaderExtent() { if (header == null) return 0.0; - assert(header.hasSize); - assert(constraints.axis != null); + assert(header!.hasSize); switch (constraints.axis) { case Axis.vertical: - return header.size.height; + return header!.size.height; case Axis.horizontal: - return header.size.width; + return header!.size.width; } - return null; } - double get headerLogicalExtent => overlapsContent ? 0.0 : _headerExtent; + double? get headerLogicalExtent => overlapsContent ? 0.0 : _headerExtent; + + double get headerPosition => sticky + ? math.min( + constraints.overlap, + (child?.geometry?.scrollExtent ?? 0.0) - + constraints.scrollOffset - + (overlapsContent ? _headerExtent! : 0.0)) + : -constraints.scrollOffset; @override void performLayout() { @@ -118,15 +163,14 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { return; } - // One of them is not null. AxisDirection axisDirection = applyGrowthDirectionToAxisDirection( constraints.axisDirection, constraints.growthDirection); if (header != null) { - header.layout( - new StickyHeaderConstraints( - state: _oldState ?? new SliverStickyHeaderState(0.0, false), - boxConstraints: constraints.asBoxConstraints(), + header!.layout( + BoxValueConstraints( + value: _oldState ?? SliverStickyHeaderState(0.0, false), + constraints: constraints.asBoxConstraints(), ), parentUsesSize: true, ); @@ -134,14 +178,14 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { } // Compute the header extent only one time. - double headerExtent = headerLogicalExtent; + double headerExtent = headerLogicalExtent!; final double headerPaintExtent = calculatePaintOffset(constraints, from: 0.0, to: headerExtent); final double headerCacheExtent = calculateCacheOffset(constraints, from: 0.0, to: headerExtent); if (child == null) { - geometry = new SliverGeometry( + geometry = SliverGeometry( scrollExtent: headerExtent, maxPaintExtent: headerExtent, paintExtent: headerPaintExtent, @@ -150,11 +194,11 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { hasVisualOverflow: headerExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0); } else { - child.layout( + child!.layout( constraints.copyWith( scrollOffset: math.max(0.0, constraints.scrollOffset - headerExtent), cacheOrigin: math.min(0.0, constraints.cacheOrigin + headerExtent), - overlap: 0.0, + overlap: math.min(headerExtent, constraints.scrollOffset), remainingPaintExtent: constraints.remainingPaintExtent - headerPaintExtent, remainingCacheExtent: @@ -162,22 +206,24 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { ), parentUsesSize: true, ); - final SliverGeometry childLayoutGeometry = child.geometry; + final SliverGeometry childLayoutGeometry = child!.geometry!; if (childLayoutGeometry.scrollOffsetCorrection != null) { - geometry = new SliverGeometry( + geometry = SliverGeometry( scrollOffsetCorrection: childLayoutGeometry.scrollOffsetCorrection, ); return; } final double paintExtent = math.min( - headerPaintExtent + - math.max(childLayoutGeometry.paintExtent, - childLayoutGeometry.layoutExtent), - constraints.remainingPaintExtent); + headerPaintExtent + + math.max(childLayoutGeometry.paintExtent, + childLayoutGeometry.layoutExtent), + constraints.remainingPaintExtent, + ); - geometry = new SliverGeometry( + geometry = SliverGeometry( scrollExtent: headerExtent + childLayoutGeometry.scrollExtent, + maxScrollObstructionExtent: sticky ? headerPaintExtent : 0, paintExtent: paintExtent, layoutExtent: math.min( headerPaintExtent + childLayoutGeometry.layoutExtent, paintExtent), @@ -191,54 +237,66 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { hasVisualOverflow: childLayoutGeometry.hasVisualOverflow, ); - final SliverPhysicalParentData childParentData = child.parentData; - assert(constraints.axisDirection != null); - assert(constraints.growthDirection != null); + final SliverPhysicalParentData? childParentData = + child!.parentData as SliverPhysicalParentData?; + switch (axisDirection) { case AxisDirection.up: - childParentData.paintOffset = Offset.zero; + // this was working ... but maybe this is getting in the way of what we should be re-positioning + if (_reverse) childParentData!.paintOffset = Offset(0.0,-headerExtent); + else childParentData!.paintOffset = Offset.zero; // reverse break; + case AxisDirection.down: + if (_reverse) childParentData!.paintOffset = Offset(0.0, + -headerExtent); + else + childParentData!.paintOffset = Offset(0.0, + calculatePaintOffset(constraints, from: 0.0, to: headerExtent)); + break; + case AxisDirection.right: - childParentData.paintOffset = new Offset( + childParentData!.paintOffset = Offset( calculatePaintOffset(constraints, from: 0.0, to: headerExtent), 0.0); break; - case AxisDirection.down: - childParentData.paintOffset = new Offset(0.0, - calculatePaintOffset(constraints, from: 0.0, to: headerExtent)); - break; case AxisDirection.left: - childParentData.paintOffset = Offset.zero; + childParentData!.paintOffset = Offset.zero; break; } } if (header != null) { - final SliverPhysicalParentData headerParentData = header.parentData; - final childScrollExtent = child?.geometry?.scrollExtent ?? 0.0; - double headerPosition = math.min( - constraints.overlap, - childScrollExtent - - constraints.scrollOffset - - (overlapsContent ? _headerExtent : 0.0)); - - _isPinned = (constraints.scrollOffset + constraints.overlap) > 0.0 || - constraints.remainingPaintExtent == - constraints.viewportMainAxisExtent; - // second layout if scroll percentage changed and header is a RenderStickyHeaderLayoutBuilder. - if (header is RenderStickyHeaderLayoutBuilder) { - double scrollPercentage = - ((headerPosition - constraints.overlap).abs() / _headerExtent) - .clamp(0.0, 1.0); + final SliverPhysicalParentData? headerParentData = + header!.parentData as SliverPhysicalParentData?; + + _isPinned = () { + if (!sticky) return false; + if (_reverse) return (constraints.remainingPaintExtent < (constraints.viewportMainAxisExtent - ((child!.parentData as SliverPhysicalParentData?)?.paintOffset.distance ?? 0))); + else return ((constraints.scrollOffset + constraints.overlap) > 0.0 || + constraints.remainingPaintExtent == + constraints.viewportMainAxisExtent); + }(); + + final double headerScrollRatio = + ((headerPosition - constraints.overlap).abs() / _headerExtent!); + if (_isPinned && headerScrollRatio <= 1) { + controller?.stickyHeaderScrollOffset = + constraints.precedingScrollExtent; + } + // second layout if scroll percentage changed and header is a + // RenderStickyHeaderLayoutBuilder. + if (header is RenderConstrainedLayoutBuilder< + BoxValueConstraints, RenderBox>) { + double headerScrollRatioClamped = headerScrollRatio.clamp(0.0, 1.0); SliverStickyHeaderState state = - new SliverStickyHeaderState(scrollPercentage, _isPinned); + SliverStickyHeaderState(headerScrollRatioClamped, _isPinned); if (_oldState != state) { _oldState = state; - header.layout( - new StickyHeaderConstraints( - state: _oldState, - boxConstraints: constraints.asBoxConstraints(), + header!.layout( + BoxValueConstraints( + value: _oldState!, + constraints: constraints.asBoxConstraints(), ), parentUsesSize: true, ); @@ -247,64 +305,80 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { switch (axisDirection) { case AxisDirection.up: - headerParentData.paintOffset = new Offset( - 0.0, geometry.paintExtent - headerPosition - _headerExtent); + double headerOffset = - headerPosition - _headerExtent!; + if (_reverse) headerParentData!.paintOffset = Offset(0.0, 0.0 + + (constraints.remainingPaintExtent < _headerExtent! ? (geometry!.paintExtent + headerOffset) + : 0)); + else headerParentData!.paintOffset = Offset(0.0, geometry!.paintExtent +headerOffset); break; case AxisDirection.down: - headerParentData.paintOffset = new Offset(0.0, headerPosition); + headerParentData?.paintOffset = Offset(0.0, headerPosition); break; case AxisDirection.left: - headerParentData.paintOffset = new Offset( - geometry.paintExtent - headerPosition - _headerExtent, 0.0); + headerParentData!.paintOffset = Offset( + geometry!.paintExtent - headerPosition - _headerExtent!, 0.0); break; case AxisDirection.right: - headerParentData.paintOffset = new Offset(headerPosition, 0.0); + headerParentData!.paintOffset = Offset(headerPosition, 0.0); break; } } } @override - bool hitTestChildren(HitTestResult result, - {@required double mainAxisPosition, @required double crossAxisPosition}) { - assert(geometry.hitTestExtent > 0.0); + bool hitTestChildren(SliverHitTestResult result, + {required double mainAxisPosition, required double crossAxisPosition}) { + assert(geometry!.hitTestExtent > 0.0); if (header != null && - mainAxisPosition - constraints.overlap <= _headerExtent) { - return hitTestBoxChild(result, header, - mainAxisPosition: mainAxisPosition - constraints.overlap, - crossAxisPosition: crossAxisPosition) || + (geometry!.paintExtent - mainAxisPosition < _headerExtent!) + ) { + final didHitHeader = hitTestBoxChild( + BoxHitTestResult.wrap(SliverHitTestResult.wrap(result)), + header!, + mainAxisPosition: geometry!.paintExtent - mainAxisPosition + childMainAxisPosition(header), + crossAxisPosition: crossAxisPosition, + ); + + return didHitHeader || (_overlapsContent && child != null && - child.geometry.hitTestExtent > 0.0 && - child.hitTest(result, + child!.geometry!.hitTestExtent > 0.0 && + child!.hitTest(result, mainAxisPosition: - mainAxisPosition - childMainAxisPosition(child), + mainAxisPosition, crossAxisPosition: crossAxisPosition)); - } else if (child != null && child.geometry.hitTestExtent > 0.0) { - return child.hitTest(result, - mainAxisPosition: mainAxisPosition - childMainAxisPosition(child), + } else if (child != null && child!.geometry!.hitTestExtent > 0.0) { + print('testing child'); + return child!.hitTest(result, + mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition); } return false; } @override - double childMainAxisPosition(RenderObject child) { - if (child == header) - return _isPinned + double childMainAxisPosition(RenderObject? child) { + if (child == header) { + if (_isPinned) return 0; + if (!_reverse) return _isPinned ? 0.0 : -(constraints.scrollOffset + constraints.overlap); - if (child == this.child) - return calculatePaintOffset(constraints, - from: 0.0, to: headerLogicalExtent); - return null; + return (constraints.scrollOffset + constraints.overlap); + } else if (child == this.child) { + return calculatePaintOffset(constraints,from: 0.0, to: headerLogicalExtent!); + } + return 0; } @override - double childScrollOffset(RenderObject child) { + double? childScrollOffset(RenderObject child) { assert(child.parent == this); if (child == this.child) { - return _headerExtent; + // if (_reverse) + return 0; + // return _headerExtent; + // } else if (_reverse && child == this._header) { + // return _headerExtent; } else { return super.childScrollOffset(child); } @@ -312,23 +386,24 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { @override void applyPaintTransform(RenderObject child, Matrix4 transform) { - assert(child != null); - final SliverPhysicalParentData childParentData = child.parentData; + final SliverPhysicalParentData childParentData = + child.parentData as SliverPhysicalParentData; childParentData.applyPaintTransform(transform); } @override void paint(PaintingContext context, Offset offset) { - if (geometry.visible) { - if (child != null && child.geometry.visible) { - final SliverPhysicalParentData childParentData = child.parentData; - context.paintChild(child, offset + childParentData.paintOffset); + if (geometry!.visible) { + final Offset? childParentDataOffset = (child == null) ? null : (child!.parentData as SliverPhysicalParentData).paintOffset; + final Offset? headerParentDataOffset = (header == null) ? null : (header!.parentData as SliverPhysicalParentData).paintOffset; + + if (child != null && child!.geometry!.visible) { + context.paintChild(child!, offset + (_reverse ? -childParentDataOffset! : childParentDataOffset!)); } - // The header must be draw over the sliver. + // The header must be drawn over the sliver. if (header != null) { - final SliverPhysicalParentData headerParentData = header.parentData; - context.paintChild(header, offset + headerParentData.paintOffset); + context.paintChild(header!, offset + headerParentDataOffset!); } } } diff --git a/lib/src/rendering/sticky_header_constraints.dart b/lib/src/rendering/sticky_header_constraints.dart deleted file mode 100644 index 3119a73..0000000 --- a/lib/src/rendering/sticky_header_constraints.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter_sticky_header/src/widgets/sliver_sticky_header.dart'; - -/// Immutable layout constraints for sticky header -class StickyHeaderConstraints extends BoxConstraints { - StickyHeaderConstraints({ - this.state, - BoxConstraints boxConstraints, - }) : assert(state != null), - assert(boxConstraints != null), - super( - minWidth: boxConstraints.minWidth, - maxWidth: boxConstraints.maxWidth, - minHeight: boxConstraints.minHeight, - maxHeight: boxConstraints.maxHeight, - ); - - final SliverStickyHeaderState state; - - @override - bool get isNormalized => - state.scrollPercentage >= 0.0 && - state.scrollPercentage <= 1.0 && - super.isNormalized; - - @override - bool operator ==(dynamic other) { - assert(debugAssertIsValid()); - if (identical(this, other)) return true; - if (other is! StickyHeaderConstraints) return false; - final StickyHeaderConstraints typedOther = other; - assert(typedOther.debugAssertIsValid()); - return state == typedOther.state && - minWidth == typedOther.minWidth && - maxWidth == typedOther.maxWidth && - minHeight == typedOther.minHeight && - maxHeight == typedOther.maxHeight; - } - - @override - int get hashCode { - assert(debugAssertIsValid()); - return hashValues(minWidth, maxWidth, minHeight, maxHeight, state); - } -} diff --git a/lib/src/rendering/sticky_header_layout_builder.dart b/lib/src/rendering/sticky_header_layout_builder.dart deleted file mode 100644 index d9a5d7c..0000000 --- a/lib/src/rendering/sticky_header_layout_builder.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_sticky_header/src/rendering/sticky_header_constraints.dart'; - -class RenderStickyHeaderLayoutBuilder extends RenderBox - with RenderObjectWithChildMixin { - RenderStickyHeaderLayoutBuilder({ - LayoutCallback callback, - }) : _callback = callback; - - LayoutCallback get callback => _callback; - LayoutCallback _callback; - set callback(LayoutCallback value) { - if (value == _callback) return; - _callback = value; - markNeedsLayout(); - } - - // layout input - @override - StickyHeaderConstraints get constraints => super.constraints; - - bool _debugThrowIfNotCheckingIntrinsics() { - assert(() { - if (!RenderObject.debugCheckingIntrinsics) { - throw new FlutterError( - 'StickyHeaderLayoutBuilder does not support returning intrinsic dimensions.\n' - 'Calculating the intrinsic dimensions would require running the layout ' - 'callback speculatively, which might mutate the live render object tree.'); - } - return true; - }()); - return true; - } - - @override - double computeMinIntrinsicWidth(double height) { - assert(_debugThrowIfNotCheckingIntrinsics()); - return 0.0; - } - - @override - double computeMaxIntrinsicWidth(double height) { - assert(_debugThrowIfNotCheckingIntrinsics()); - return 0.0; - } - - @override - double computeMinIntrinsicHeight(double width) { - assert(_debugThrowIfNotCheckingIntrinsics()); - return 0.0; - } - - @override - double computeMaxIntrinsicHeight(double width) { - assert(_debugThrowIfNotCheckingIntrinsics()); - return 0.0; - } - - @override - void performLayout() { - assert(callback != null); - invokeLayoutCallback(callback); - if (child != null) { - child.layout(constraints, parentUsesSize: true); - size = constraints.constrain(child.size); - } else { - size = constraints.biggest; - } - } - - @override - bool hitTestChildren(HitTestResult result, {Offset position}) { - return child?.hitTest(result, position: position) ?? false; - } - - @override - void paint(PaintingContext context, Offset offset) { - if (child != null) context.paintChild(child, offset); - } -} diff --git a/lib/src/widgets/sliver_sticky_header.dart b/lib/src/widgets/sliver_sticky_header.dart index 6937505..7372f80 100644 --- a/lib/src/widgets/sliver_sticky_header.dart +++ b/lib/src/widgets/sliver_sticky_header.dart @@ -1,11 +1,112 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_sticky_header/src/rendering/sliver_sticky_header.dart'; -import 'package:flutter_sticky_header/src/widgets/sticky_header_layout_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +// import 'package:flutter_sticky_header/src/rendering/sliver_sticky_header.dart'; +import '../rendering/sliver_sticky_header.dart'; +import 'package:value_layout_builder/value_layout_builder.dart'; -/// Signature used by [SliverStickyHeaderBuilder] to build the header +/// Signature used by [SliverStickyHeader.builder] to build the header /// when the sticky header state has changed. typedef Widget SliverStickyHeaderWidgetBuilder( - BuildContext context, SliverStickyHeaderState state); + BuildContext context, + SliverStickyHeaderState state, +); + +/// A +class StickyHeaderController with ChangeNotifier { + /// The offset to use in order to jump to the first item + /// of current the sticky header. + /// + /// If there is no sticky headers, this is 0. + double get stickyHeaderScrollOffset => _stickyHeaderScrollOffset; + double _stickyHeaderScrollOffset = 0; + + /// This setter should only be used by flutter_sticky_header package. + set stickyHeaderScrollOffset(double value) { + if (_stickyHeaderScrollOffset != value) { + _stickyHeaderScrollOffset = value; + notifyListeners(); + } + } +} + +/// The [StickyHeaderController] for descendant widgets that don't specify one +/// explicitly. +/// +/// [DefaultStickyHeaderController] is an inherited widget that is used to share a +/// [StickyHeaderController] with [SliverStickyHeader]s. It's used when sharing an +/// explicitly created [StickyHeaderController] isn't convenient because the sticky +/// headers are created by a stateless parent widget or by different parent +/// widgets. +class DefaultStickyHeaderController extends StatefulWidget { + const DefaultStickyHeaderController({ + Key? key, + required this.child, + }) : super(key: key); + + /// The widget below this widget in the tree. + /// + /// Typically a [Scaffold] whose [AppBar] includes a [TabBar]. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// The closest instance of this class that encloses the given context. + /// + /// Typical usage: + /// + /// ```dart + /// StickyHeaderController controller = DefaultStickyHeaderController.of(context); + /// ``` + static StickyHeaderController? of(BuildContext context) { + final _StickyHeaderControllerScope? scope = context + .dependOnInheritedWidgetOfExactType<_StickyHeaderControllerScope>(); + return scope?.controller; + } + + @override + _DefaultStickyHeaderControllerState createState() => + _DefaultStickyHeaderControllerState(); +} + +class _DefaultStickyHeaderControllerState + extends State { + StickyHeaderController? _controller; + + @override + void initState() { + super.initState(); + _controller = StickyHeaderController(); + } + + @override + void dispose() { + _controller!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _StickyHeaderControllerScope( + controller: _controller, + child: widget.child, + ); + } +} + +class _StickyHeaderControllerScope extends InheritedWidget { + const _StickyHeaderControllerScope({ + Key? key, + this.controller, + required Widget child, + }) : super(key: key, child: child); + + final StickyHeaderController? controller; + + @override + bool updateShouldNotify(_StickyHeaderControllerScope old) { + return controller != old.controller; + } +} /// State describing how a sticky header is rendered. @immutable @@ -13,8 +114,7 @@ class SliverStickyHeaderState { const SliverStickyHeaderState( this.scrollPercentage, this.isPinned, - ) : assert(scrollPercentage != null), - assert(isPinned != null); + ); final double scrollPercentage; @@ -40,44 +140,99 @@ class SliverStickyHeaderState { /// /// Place this widget inside a [CustomScrollView] or similar. class SliverStickyHeader extends RenderObjectWidget { - /// Creates a sliver that displays the [header] before its [sliver], unless [overlapsContent] it's true. + /// Creates a sliver that displays the [header] before its [sliver], unless + /// [overlapsContent] it's true. /// The [header] stays pinned when it hits the start of the viewport until /// the [sliver] scrolls off the viewport. /// - /// The [overlapsContent] argument must not be null. + /// The [overlapsContent] and [sticky] arguments must not be null. + /// + /// If a [StickyHeaderController] is not provided, then the value of + /// [DefaultStickyHeaderController.of] will be used. SliverStickyHeader({ - Key key, + Key? key, this.header, this.sliver, this.overlapsContent: false, - }) : assert(overlapsContent != null), - super(key: key); + this.sticky = true, + this.reverse = false, + this.controller, + }) : super(key: key); + + /// Creates a widget that builds the header of a [SliverStickyHeader] + /// each time its scroll percentage changes. + /// + /// The [builder], [overlapsContent] and [sticky] arguments must not be null. + /// + /// If a [StickyHeaderController] is not provided, then the value of + /// [DefaultStickyHeaderController.of] will be used. + SliverStickyHeader.builder({ + Key? key, + required SliverStickyHeaderWidgetBuilder builder, + Widget? sliver, + bool overlapsContent: false, + bool sticky = true, + bool reverse = false, + StickyHeaderController? controller, + }) : this( + key: key, + header: ValueLayoutBuilder( + builder: (context, constraints) => + builder(context, constraints.value), + ), + sliver: sliver, + overlapsContent: overlapsContent, + sticky: sticky, + reverse: reverse, + controller: controller, + ); /// The header to display before the sliver. - final Widget header; + final Widget? header; /// The sliver to display after the header. - final Widget sliver; + final Widget? sliver; /// Whether the header should be drawn on top of the sliver /// instead of before. final bool overlapsContent; + /// Whether to stick the header. + /// Defaults to true. + final bool sticky; + + final bool reverse; + + /// The controller used to interact with this sliver. + /// + /// If a [StickyHeaderController] is not provided, then the value of [DefaultStickyHeaderController.of] + /// will be used. + final StickyHeaderController? controller; + @override RenderSliverStickyHeader createRenderObject(BuildContext context) { - return new RenderSliverStickyHeader( + return RenderSliverStickyHeader( overlapsContent: overlapsContent, + sticky: sticky, + reverse: reverse, + controller: controller ?? DefaultStickyHeaderController.of(context), ); } @override SliverStickyHeaderRenderObjectElement createElement() => - new SliverStickyHeaderRenderObjectElement(this); + SliverStickyHeaderRenderObjectElement(this); @override void updateRenderObject( - BuildContext context, RenderSliverStickyHeader renderObject) { - renderObject..overlapsContent = overlapsContent; + BuildContext context, + RenderSliverStickyHeader renderObject, + ) { + renderObject + ..overlapsContent = overlapsContent + ..sticky = sticky + ..reverse = reverse + ..controller = controller ?? DefaultStickyHeaderController.of(context); } } @@ -87,19 +242,24 @@ class SliverStickyHeader extends RenderObjectWidget { /// This is useful if you want to change the header layout when it starts to scroll off the viewport. /// You cannot change the main axis extent of the header in this builder otherwise it could result /// in strange behavior. +@Deprecated('Use SliverStickyHeader.builder instead.') class SliverStickyHeaderBuilder extends StatelessWidget { /// Creates a widget that builds the header of a [SliverStickyHeader] /// each time its scroll percentage changes. /// - /// The [builder] and [overlapsContent] arguments must not be null. + /// The [builder], [overlapsContent] and [sticky] arguments must not be null. + /// + /// If a [StickyHeaderController] is not provided, then the value of [DefaultStickyHeaderController.of] + /// will be used. const SliverStickyHeaderBuilder({ - Key key, - @required this.builder, + Key? key, + required this.builder, this.sliver, this.overlapsContent: false, - }) : assert(builder != null), - assert(overlapsContent != null), - super(key: key); + this.sticky = true, + this.reverse = false, + this.controller, + }) : super(key: key); /// Called to build the [SliverStickyHeader]'s header. /// @@ -108,19 +268,34 @@ class SliverStickyHeaderBuilder extends StatelessWidget { final SliverStickyHeaderWidgetBuilder builder; /// The sliver to display after the header. - final Widget sliver; + final Widget? sliver; /// Whether the header should be drawn on top of the sliver /// instead of before. final bool overlapsContent; + /// Whether to stick the header. + /// Defaults to true. + final bool sticky; + + final bool reverse; + + /// The controller used to interact with this sliver. + /// + /// If a [StickyHeaderController] is not provided, then the value of [DefaultStickyHeaderController.of] + /// will be used. + final StickyHeaderController? controller; + @override Widget build(BuildContext context) { - return new SliverStickyHeader( + return SliverStickyHeader( overlapsContent: overlapsContent, sliver: sliver, - header: new StickyHeaderLayoutBuilder( - builder: (context, constraints) => builder(context, constraints.state), + sticky: sticky, + reverse: reverse, + controller: controller, + header: ValueLayoutBuilder( + builder: (context, constraints) => builder(context, constraints.value), ), ); } @@ -132,26 +307,27 @@ class SliverStickyHeaderRenderObjectElement extends RenderObjectElement { : super(widget); @override - SliverStickyHeader get widget => super.widget; + SliverStickyHeader get widget => super.widget as SliverStickyHeader; - Element _header; + Element? _header; - Element _sliver; + Element? _sliver; @override void visitChildren(ElementVisitor visitor) { - if (_header != null) visitor(_header); - if (_sliver != null) visitor(_sliver); + if (_header != null) visitor(_header!); + if (_sliver != null) visitor(_sliver!); } @override void forgetChild(Element child) { + super.forgetChild(child); if (child == _header) _header = null; if (child == _sliver) _sliver = null; } @override - void mount(Element parent, dynamic newSlot) { + void mount(Element? parent, dynamic newSlot) { super.mount(parent, newSlot); _header = updateChild(_header, widget.header, 0); _sliver = updateChild(_sliver, widget.sliver, 1); @@ -166,21 +342,23 @@ class SliverStickyHeaderRenderObjectElement extends RenderObjectElement { } @override - void insertChildRenderObject(RenderObject child, int slot) { - final RenderSliverStickyHeader renderObject = this.renderObject; - if (slot == 0) renderObject.header = child; - if (slot == 1) renderObject.child = child; + void insertRenderObjectChild(RenderObject child, int? slot) { + final RenderSliverStickyHeader renderObject = + this.renderObject as RenderSliverStickyHeader; + if (slot == 0) renderObject.header = child as RenderBox?; + if (slot == 1) renderObject.child = child as RenderSliver?; assert(renderObject == this.renderObject); } @override - void moveChildRenderObject(RenderObject child, slot) { + void moveRenderObjectChild(RenderObject child, slot, newSlot) { assert(false); } @override - void removeChildRenderObject(RenderObject child) { - final RenderSliverStickyHeader renderObject = this.renderObject; + void removeRenderObjectChild(RenderObject child, slot) { + final RenderSliverStickyHeader renderObject = + this.renderObject as RenderSliverStickyHeader; if (renderObject.header == child) renderObject.header = null; if (renderObject.child == child) renderObject.child = null; assert(renderObject == this.renderObject); diff --git a/lib/src/widgets/sticky_header_layout_builder.dart b/lib/src/widgets/sticky_header_layout_builder.dart deleted file mode 100644 index 984a328..0000000 --- a/lib/src/widgets/sticky_header_layout_builder.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_sticky_header/src/rendering/sticky_header_constraints.dart'; -import 'package:flutter_sticky_header/src/rendering/sticky_header_layout_builder.dart'; - -typedef Widget StickyHeaderLayoutWidgetBuilder( - BuildContext context, StickyHeaderConstraints constraints); - -/// Builds a widget tree that can depend on the [StickyHeaderConstraints]. -/// -/// This is used by [SliverStickyHeaderBuilder] to change the header layout -/// while it starts to scroll off the viewport. -class StickyHeaderLayoutBuilder extends RenderObjectWidget { - /// Creates a widget that defers its building until layout. - /// - /// The [builder] argument must not be null. - const StickyHeaderLayoutBuilder({ - Key key, - @required this.builder, - }) : assert(builder != null), - super(key: key); - - /// Called at layout time to construct the widget tree. The builder must not - /// return null. - final StickyHeaderLayoutWidgetBuilder builder; - - @override - RenderObjectElement createElement() => - new _StickyHeaderLayoutBuilderElement(this); - - @override - RenderObject createRenderObject(BuildContext context) => - new RenderStickyHeaderLayoutBuilder(); - - // updateRenderObject is redundant with the logic in the _StickyHeaderLayoutBuilderElement below. -} - -class _StickyHeaderLayoutBuilderElement extends RenderObjectElement { - _StickyHeaderLayoutBuilderElement(StickyHeaderLayoutBuilder widget) - : super(widget); - - @override - StickyHeaderLayoutBuilder get widget => super.widget; - - @override - RenderStickyHeaderLayoutBuilder get renderObject => super.renderObject; - - Element _child; - - @override - void visitChildren(ElementVisitor visitor) { - if (_child != null) visitor(_child); - } - - @override - void forgetChild(Element child) { - assert(child == _child); - _child = null; - } - - @override - void mount(Element parent, dynamic newSlot) { - super.mount(parent, newSlot); // Creates the renderObject. - renderObject.callback = _layout; - } - - @override - void update(StickyHeaderLayoutBuilder newWidget) { - assert(widget != newWidget); - super.update(newWidget); - assert(widget == newWidget); - renderObject.callback = _layout; - renderObject.markNeedsLayout(); - } - - @override - void performRebuild() { - // This gets called if markNeedsBuild() is called on us. - // That might happen if, e.g., our builder uses Inherited widgets. - renderObject.markNeedsLayout(); - super - .performRebuild(); // Calls widget.updateRenderObject (a no-op in this case). - } - - @override - void unmount() { - renderObject.callback = null; - super.unmount(); - } - - void _layout(StickyHeaderConstraints constraints) { - owner.buildScope(this, () { - Widget built; - if (widget.builder != null) { - try { - built = widget.builder(this, constraints); - debugWidgetBuilderValue(widget, built); - } catch (e, stack) { - built = ErrorWidget - .builder(_debugReportException('building $widget', e, stack)); - } - } - try { - _child = updateChild(_child, built, null); - assert(_child != null); - } catch (e, stack) { - built = ErrorWidget - .builder(_debugReportException('building $widget', e, stack)); - _child = updateChild(null, built, slot); - } - }); - } - - @override - void insertChildRenderObject(RenderObject child, slot) { - final RenderObjectWithChildMixin renderObject = - this.renderObject; - assert(slot == null); - assert(renderObject.debugValidateChild(child)); - renderObject.child = child; - assert(renderObject == this.renderObject); - } - - @override - void moveChildRenderObject(RenderObject child, slot) { - assert(false); - } - - @override - void removeChildRenderObject(RenderObject child) { - final RenderStickyHeaderLayoutBuilder renderObject = this.renderObject; - assert(renderObject.child == child); - renderObject.child = null; - assert(renderObject == this.renderObject); - } -} - -FlutterErrorDetails _debugReportException( - String context, - dynamic exception, - StackTrace stack, -) { - final FlutterErrorDetails details = new FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'flutter_sticky_header widgets library', - context: context); - FlutterError.reportError(details); - return details; -} diff --git a/pubspec.yaml b/pubspec.yaml index 77e2f69..bfa2b5a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,18 @@ name: flutter_sticky_header description: Flutter implementation of sticky headers as a sliver. Use it in a CustomScrollView. -version: 0.3.4 -author: Romain Rastel +version: 0.6.4 homepage: https://github.com/letsar/flutter_sticky_header dependencies: flutter: sdk: flutter + value_layout_builder: ^0.3.0 dev_dependencies: flutter_test: sdk: flutter + sliver_tools: ^0.2.7 environment: - sdk: ">=1.19.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' + flutter: '>=1.24.0-10.2.pre' diff --git a/test/controller_test.dart b/test/controller_test.dart new file mode 100644 index 0000000..d5ee534 --- /dev/null +++ b/test/controller_test.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + setUp(() { + WidgetsBinding.instance.renderView.configuration = + TestViewConfiguration(size: Size(400, 800)); + }); + + testWidgets('StickyHeaderController.stickyHeaderScrollOffset', + (WidgetTester tester) async { + final StickyHeaderController stickyHeaderController = + StickyHeaderController(); + final ScrollController scrollController = ScrollController(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomScrollView( + cacheExtent: 0, + controller: scrollController, + slivers: [ + SliverStickyHeader( + header: _Header(index: 0), + sliver: const _Sliver(), + controller: stickyHeaderController, + ), + SliverStickyHeader( + header: _Header(index: 1), + sliver: const _Sliver(), + controller: stickyHeaderController, + ), + SliverStickyHeader( + header: _Header(index: 2), + sliver: const _Sliver(), + controller: stickyHeaderController, + ), + ], + ), + ), + ), + ); + + final header00Finder = find.text('Header #0'); + final header01Finder = find.text('Header #1'); + final header02Finder = find.text('Header #2'); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsNothing); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(0)); + + final gesture = await tester.startGesture(Offset(200, 100)); + + // We scroll just before the Header #1. + await gesture.moveBy(Offset(0, -80)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsNothing); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(0)); + + // We scroll just after the Header #1 so that it is visible. + await gesture.moveBy(Offset(0, -80)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(0)); + + // We scroll in a way that Headers 0 and 1 are side by side. + await gesture.moveBy(Offset(0, -640)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(0)); + + // We scroll in a way that Header #1 is at the top of the screen. + await gesture.moveBy(Offset(0, -80)); + await tester.pump(); + + expect(header00Finder, findsNothing); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(880)); + + // We scroll in a way that Header #1 is not visible. + await gesture.moveBy(Offset(0, -80)); + await tester.pump(); + + expect(header00Finder, findsNothing); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(880)); + }); + + testWidgets('StickyHeaderController.stickyHeaderScrollOffset - reverse', + (WidgetTester tester) async { + final StickyHeaderController stickyHeaderController = + StickyHeaderController(); + final ScrollController scrollController = ScrollController(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomScrollView( + controller: scrollController, + cacheExtent: 0, + reverse: true, + slivers: [ + SliverStickyHeader( + header: _Header(index: 0), + sliver: const _Sliver(), + controller: stickyHeaderController, + ), + SliverStickyHeader( + header: _Header(index: 1), + sliver: const _Sliver(), + controller: stickyHeaderController, + ), + SliverStickyHeader( + header: _Header(index: 2), + sliver: const _Sliver(), + controller: stickyHeaderController, + ), + ], + ), + ), + ), + ); + + final header00Finder = find.text('Header #0'); + final header01Finder = find.text('Header #1'); + final header02Finder = find.text('Header #2'); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsNothing); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(0)); + + final gesture = await tester.startGesture(Offset(200, 100)); + + // We scroll just before the Header #1. + await gesture.moveBy(Offset(0, 80)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsNothing); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(0)); + + // We scroll just after the Header #1 so that it is visible. + await gesture.moveBy(Offset(0, 80)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(0)); + + // We scroll in a way that Headers 0 and 1 are side by side. + await gesture.moveBy(Offset(0, 640)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(0)); + + // We scroll in a way that Header #1 is at the top of the screen. + await gesture.moveBy(Offset(0, 80)); + await tester.pump(); + + expect(header00Finder, findsNothing); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(880)); + + // We scroll in a way that Header #1 is no longer visible. + await gesture.moveBy(Offset(0, 80)); + await tester.pump(); + + expect(header00Finder, findsNothing); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + expect(stickyHeaderController.stickyHeaderScrollOffset, equals(880)); + }); +} + +class _Header extends StatelessWidget { + const _Header({ + Key? key, + required this.index, + }) : super(key: key); + + final int index; + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.blue, + child: Text('Header #$index'), + height: 80, + ); + } +} + +class _Sliver extends StatelessWidget { + const _Sliver({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => const _SliverItem(), + childCount: 20, + ), + ); + } +} + +class _SliverItem extends StatelessWidget { + const _SliverItem({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox(height: 40); + } +} diff --git a/test/flutter_sticky_header_test.dart b/test/flutter_sticky_header_test.dart deleted file mode 100644 index ab73b3a..0000000 --- a/test/flutter_sticky_header_test.dart +++ /dev/null @@ -1 +0,0 @@ -void main() {} diff --git a/test/sticky_test.dart b/test/sticky_test.dart new file mode 100644 index 0000000..402f34e --- /dev/null +++ b/test/sticky_test.dart @@ -0,0 +1,341 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sliver_tools/sliver_tools.dart'; + +void main() { + setUp(() { + WidgetsBinding.instance.renderView.configuration = + TestViewConfiguration(size: Size(400, 800)); + }); + + testWidgets('Mix sticky and not sticky headers', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomScrollView( + cacheExtent: 0, + slivers: [ + SliverStickyHeader( + header: _Header(index: 0), + sliver: const _Sliver(), + ), + SliverStickyHeader( + header: _Header(index: 1), + sticky: false, + sliver: const _Sliver(), + ), + SliverStickyHeader( + header: _Header(index: 2), + sliver: const _Sliver(), + ), + ], + ), + ), + ), + ); + + final header00Finder = find.text('Header #0'); + final header01Finder = find.text('Header #1'); + final header02Finder = find.text('Header #2'); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsNothing); + expect(header02Finder, findsNothing); + + final gesture = await tester.startGesture(Offset(200, 100)); + + // We scroll just before the Header #1. + await gesture.moveBy(Offset(0, -80)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsNothing); + expect(header02Finder, findsNothing); + + // We scroll just after the Header #1 so that it is visible. + await gesture.moveBy(Offset(0, -80)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + + // We scroll in a way that Headers 0 and 1 are side by side. + await gesture.moveBy(Offset(0, -640)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + + // We scroll in a way that Header #1 is at the top of the screen. + await gesture.moveBy(Offset(0, -80)); + await tester.pump(); + + expect(header00Finder, findsNothing); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + + // We scroll in a way that Header #1 is not visible. + await gesture.moveBy(Offset(0, -80)); + await tester.pump(); + + expect(header00Finder, findsNothing); + // Header #1 is in the tree (because the sliver is onstage). + expect(tester.getRect(header01Finder), const Rect.fromLTRB(0, -80, 400, 0)); + expect(header02Finder, findsNothing); + }); + + testWidgets('Mix sticky and not sticky headers - reverse', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomScrollView( + cacheExtent: 0, + reverse: true, + slivers: [ + SliverStickyHeader( + header: _Header(index: 0), + sliver: const _Sliver(), + ), + SliverStickyHeader( + header: _Header(index: 1), + sticky: false, + sliver: const _Sliver(), + ), + SliverStickyHeader( + header: _Header(index: 2), + sliver: const _Sliver(), + ), + ], + ), + ), + ), + ); + + final header00Finder = find.text('Header #0'); + final header01Finder = find.text('Header #1'); + final header02Finder = find.text('Header #2'); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsNothing); + expect(header02Finder, findsNothing); + + final gesture = await tester.startGesture(Offset(200, 100)); + + // We scroll just before the Header #1. + await gesture.moveBy(Offset(0, 80)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsNothing); + expect(header02Finder, findsNothing); + + // We scroll just after the Header #1 so that it is visible. + await gesture.moveBy(Offset(0, 80)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + + // We scroll in a way that Headers 0 and 1 are side by side. + await gesture.moveBy(Offset(0, 640)); + await tester.pump(); + + expect(header00Finder, findsOneWidget); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + + // We scroll in a way that Header #1 is at the top of the screen. + await gesture.moveBy(Offset(0, 80)); + await tester.pump(); + + expect(header00Finder, findsNothing); + expect(header01Finder, findsOneWidget); + expect(header02Finder, findsNothing); + + // We scroll in a way that Header #1 is no longer visible. + await gesture.moveBy(Offset(0, 80)); + await tester.pump(); + + expect(header00Finder, findsNothing); + // Header #1 is in the tree (because the sliver is onstage). + expect( + tester.getRect(header01Finder), const Rect.fromLTRB(0, 800, 400, 880)); + expect(header02Finder, findsNothing); + }); + + testWidgets('Testing multi-depth sticky headers', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomScrollView( + cacheExtent: 0, + slivers: [ + SliverStickyHeader( + header: _HierarchyHeader(hierarchy: '1'), + sliver: MultiSliver( + children: [ + SliverStickyHeader( + header: _HierarchyHeader(hierarchy: '1.1'), + sliver: const _Sliver100(), + ), + SliverStickyHeader( + header: _HierarchyHeader(hierarchy: '1.2'), + sliver: MultiSliver( + children: [ + SliverStickyHeader( + header: _HierarchyHeader(hierarchy: '1.2.1'), + sliver: const _Sliver100(), + ), + SliverStickyHeader( + header: _HierarchyHeader(hierarchy: '1.2.2'), + sliver: const _Sliver100(), + ), + ], + ), + ), + ], + ), + ), + SliverStickyHeader( + header: _HierarchyHeader(hierarchy: '2'), + sliver: const _Sliver100(), + ), + SliverStickyHeader( + header: _HierarchyHeader(hierarchy: '3'), + sliver: MultiSliver( + children: [ + SliverStickyHeader( + header: _HierarchyHeader(hierarchy: '3.1'), + sliver: const _Sliver100(), + ), + SliverStickyHeader( + header: _HierarchyHeader(hierarchy: '3.2'), + sliver: const _Sliver100(), + ), + ], + ), + ), + ], + ), + ), + ), + ); + + final header001Finder = find.text('Header 1'); + final header011Finder = find.text('Header 1.1'); + final header012Finder = find.text('Header 1.2'); + final header121Finder = find.text('Header 1.2.1'); + + expect(header001Finder, findsOneWidget); + expect(header011Finder, findsOneWidget); + expect(header012Finder, findsOneWidget); + expect(header121Finder, findsOneWidget); + + expect(tester.getTopLeft(header011Finder).dy, 50); + expect(tester.getTopLeft(header012Finder).dy, 200); + expect(tester.getTopLeft(header121Finder).dy, 250); + + // We scroll a little and expect that header 1 is sticky. + final gesture = await tester.startGesture(Offset(200, 100)); + await gesture.moveBy(Offset(0, -25)); + await tester.pump(); + + expect(tester.getTopLeft(header011Finder).dy, 50); + expect(tester.getTopLeft(header012Finder).dy, 175); + expect(tester.getTopLeft(header121Finder).dy, 225); + + await gesture.moveBy(Offset(0, -125)); + await tester.pump(); + + expect(tester.getTopLeft(header011Finder).dy, 0); + expect(tester.getTopLeft(header012Finder).dy, 50); + expect(tester.getTopLeft(header121Finder).dy, 100); + + await gesture.moveBy(Offset(0, -25)); + await tester.pump(); + + expect(tester.getTopLeft(header012Finder).dy, 50); + expect(tester.getTopLeft(header121Finder).dy, 100); + }); +} + +class _HierarchyHeader extends StatelessWidget { + const _HierarchyHeader({ + Key? key, + required this.hierarchy, + }) : super(key: key); + + final String hierarchy; + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.blue, + child: Text('Header $hierarchy'), + height: 50, + ); + } +} + +class _Sliver100 extends StatelessWidget { + const _Sliver100({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: SizedBox(height: 100), + ); + } +} + +class _Header extends StatelessWidget { + const _Header({ + Key? key, + required this.index, + }) : super(key: key); + + final int index; + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.blue, + child: Text('Header #$index'), + height: 80, + ); + } +} + +class _Sliver extends StatelessWidget { + const _Sliver({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => const _SliverItem(), + childCount: 20, + ), + ); + } +} + +class _SliverItem extends StatelessWidget { + const _SliverItem({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox(height: 40); + } +}