├── .conventional_commits ├── .github └── workflows │ └── deploy_github_pages.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── design └── feature_graphic.png ├── example ├── .gitignore ├── .metadata ├── EXAMPLE.md ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── dev │ │ │ │ │ └── chulwoo │ │ │ │ │ └── timelines │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── assets │ └── images │ │ ├── package_delivery_tracking.png │ │ ├── process_timeline.png │ │ ├── process_timeline │ │ ├── status1.png │ │ ├── status2.png │ │ ├── status3.png │ │ ├── status4.png │ │ └── status5.png │ │ └── timeline_status.png ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── component_page.dart │ ├── main.dart │ ├── showcase │ │ ├── package_delivery_tracking.dart │ │ ├── process_timeline.dart │ │ └── timeline_status.dart │ ├── showcase_page.dart │ ├── theme_page.dart │ └── widget.dart ├── pubspec.yaml ├── test │ └── widget_test.dart └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ └── Icon-512.png │ ├── index.html │ └── manifest.json ├── lib ├── src │ ├── connector_theme.dart │ ├── connectors.dart │ ├── indicator_theme.dart │ ├── indicators.dart │ ├── line_painter.dart │ ├── timeline_node.dart │ ├── timeline_theme.dart │ ├── timeline_tile.dart │ ├── timeline_tile_builder.dart │ ├── timelines.dart │ └── util.dart └── timelines.dart ├── pubspec.yaml ├── screenshots ├── complex_timeline_node.png ├── connection_direction_after.png ├── connection_direction_before.png ├── container_indicator.png ├── contents_align_alternating.png ├── contents_align_basic.png ├── contents_align_reverse.png ├── dashed_line_connector.png ├── decorated_line_connector.png ├── dot_indicator.png ├── example.png ├── outlined_dot_indicator.png ├── package_delivery_tracking.gif ├── process_timeline.gif ├── simple_timeline_node.png ├── solid_line_connector.png ├── timeline_status.gif └── timeline_tile.png └── test └── timelines_test.dart /.conventional_commits: -------------------------------------------------------------------------------- 1 | 2 | # 3 | #------------------------------------------------------------------------------- 4 | # 5 | # feat - A new feature 6 | # fix - A bug fix 7 | # docs - Documentation only changes 8 | # style - Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 9 | # refactor - A code change that neither fixes a bug nor adds a feature 10 | # perf - A code change that improves performance 11 | # test - Adding missing tests or correcting existing tests 12 | # build - Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) 13 | # ci - Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) 14 | # chore - Other changes that don't modify src or test files 15 | # revert - Reverts a previous commit 16 | # bump - Version update 17 | # localize - Translations update 18 | # 19 | # 20 | # Conventional Commits v1.0.0 21 | # - Check out the details here: conventionalcommits.org 22 | # 23 | # 24 | # (): 25 | # ^--^ ^----------^ 26 | # | | 27 | # | +-> Summary in present tense. 28 | # | 29 | # +-------> Type: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, bump, or localize. 30 | # 31 | # is optional 32 | # 33 | # 34 | # Each type is referenced here: 35 | # - joshbuchea's semantic-commit-messages.md 36 | # 37 | #------------------------------------------------------------------------------- 38 | # 39 | -------------------------------------------------------------------------------- /.github/workflows/deploy_github_pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy github pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build: 8 | name: Build Web 9 | env: 10 | my_secret: ${{secrets.commit_secret}} 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v1 14 | - uses: subosito/flutter-action@v1 15 | with: 16 | channel: 'beta' 17 | - run: flutter config --enable-web 18 | - run: flutter pub get 19 | - run: | 20 | cd example 21 | flutter build web --release 22 | - run: | 23 | cd example/build/web 24 | git init 25 | git config --global user.email park@chulwoo.dev 26 | git config --global user.name chulwoo.park 27 | git status 28 | git remote add origin https://${{secrets.commit_secret}}@github.com/chulwoo-park/timelines.git 29 | git checkout -b gh-pages 30 | git add --all 31 | git commit -m "Update gh-pages" 32 | git push origin gh-pages -f 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .classpath 21 | .project 22 | .settings/ 23 | .vscode/ 24 | 25 | # Flutter repo-specific 26 | /bin/cache/ 27 | /bin/internal/bootstrap.bat 28 | /bin/internal/bootstrap.sh 29 | /bin/mingit/ 30 | /dev/benchmarks/mega_gallery/ 31 | /dev/bots/.recipe_deps 32 | /dev/bots/android_tools/ 33 | /dev/devicelab/ABresults*.json 34 | /dev/docs/doc/ 35 | /dev/docs/flutter.docs.zip 36 | /dev/docs/lib/ 37 | /dev/docs/pubspec.yaml 38 | /dev/integration_tests/**/xcuserdata 39 | /dev/integration_tests/**/Pods 40 | /packages/flutter/coverage/ 41 | version 42 | analysis_benchmark.json 43 | 44 | # packages file containing multi-root paths 45 | .packages.generated 46 | 47 | # Flutter/Dart/Pub related 48 | **/doc/api/ 49 | .dart_tool/ 50 | .flutter-plugins 51 | .flutter-plugins-dependencies 52 | **/generated_plugin_registrant.dart 53 | .packages 54 | .pub-cache/ 55 | .pub/ 56 | build/ 57 | flutter_*.png 58 | linked_*.ds 59 | unlinked.ds 60 | unlinked_spec.ds 61 | 62 | # Android related 63 | **/android/**/gradle-wrapper.jar 64 | **/android/.gradle 65 | **/android/captures/ 66 | **/android/gradlew 67 | **/android/gradlew.bat 68 | **/android/local.properties 69 | **/android/**/GeneratedPluginRegistrant.java 70 | **/android/key.properties 71 | *.jks 72 | 73 | # iOS/XCode related 74 | **/ios/**/*.mode1v3 75 | **/ios/**/*.mode2v3 76 | **/ios/**/*.moved-aside 77 | **/ios/**/*.pbxuser 78 | **/ios/**/*.perspectivev3 79 | **/ios/**/*sync/ 80 | **/ios/**/.sconsign.dblite 81 | **/ios/**/.tags* 82 | **/ios/**/.vagrant/ 83 | **/ios/**/DerivedData/ 84 | **/ios/**/Icon? 85 | **/ios/**/Pods/ 86 | **/ios/**/.symlinks/ 87 | **/ios/**/profile 88 | **/ios/**/xcuserdata 89 | **/ios/.generated/ 90 | **/ios/Flutter/.last_build_id 91 | **/ios/Flutter/App.framework 92 | **/ios/Flutter/Flutter.framework 93 | **/ios/Flutter/Flutter.podspec 94 | **/ios/Flutter/Generated.xcconfig 95 | **/ios/Flutter/app.flx 96 | **/ios/Flutter/app.zip 97 | **/ios/Flutter/flutter_assets/ 98 | **/ios/Flutter/flutter_export_environment.sh 99 | **/ios/ServiceDefinitions.json 100 | **/ios/Runner/GeneratedPluginRegistrant.* 101 | 102 | # macOS 103 | **/macos/Flutter/GeneratedPluginRegistrant.swift 104 | **/macos/Flutter/Flutter-Debug.xcconfig 105 | **/macos/Flutter/Flutter-Release.xcconfig 106 | **/macos/Flutter/Flutter-Profile.xcconfig 107 | 108 | # Coverage 109 | coverage/ 110 | 111 | # Symbols 112 | app.*.symbols 113 | 114 | # Exceptions to above rules. 115 | !**/ios/**/default.mode1v3 116 | !**/ios/**/default.mode2v3 117 | !**/ios/**/default.pbxuser 118 | !**/ios/**/default.perspectivev3 119 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 120 | !/dev/ci/**/Gemfile.lock 121 | 122 | .fvm -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8874f21e79d7ec66d0457c7ab338348e31b17f1d 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | 3 | * Migrate to null safety(thanks to @areille #33) 4 | 5 | ## 0.0.2+1 6 | 7 | * Dartfmt line length changed from 120 to 80 8 | 9 | 10 | ## 0.0.2 11 | 12 | ### Add basic components 13 | 14 | #### Timeline 15 | 16 | * TimelineTheme 17 | * Timeline 18 | * FixedTimeline 19 | 20 | #### TimelineTile 21 | 22 | * TimelineTile 23 | * TimelineTileBuilder 24 | 25 | #### TimelineNode 26 | 27 | * TimelineNode 28 | 29 | #### Connector 30 | 31 | * ConnectorTheme 32 | * Connector 33 | * SolidLineConnector 34 | * DecoratedLineConnector 35 | * DashedLineConnector 36 | * TransparentConnector 37 | 38 | #### Indicator 39 | 40 | * IndicatorTheme 41 | * ContainerIndicator 42 | * DotIndicator 43 | * OutlinedDotIndicator 44 | 45 | ### Add demo web 46 | 47 | * [Demo web](https://chulwoo.dev/timelines) 48 | 49 | 50 | ## 0.0.1 51 | 52 | * Initial release. 53 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@chulwoo.dev. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Chulwoo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![banner](https://raw.github.com/chulwoo-park/timelines/main/design/feature_graphic.png)](https://github.com/chulwoo-park/timelines) 2 | 3 |

4 | 5 | Pub 6 | 7 | 8 | Awesome Flutter 9 | 10 | 11 | License: MIT 12 | 13 |

14 | 15 |

A powerful & easy to use timeline package for Flutter! 🚀

16 | 17 | > ***Caveat***: This package is an early stage. Not enough testing has been done to guarantee stability. Some APIs may change. 18 | 19 | # Examples 20 | 21 | Check it out on the [web](https://chulwoo.dev/timelines/) or look at the [source code](https://github.com/chulwoo-park/timelines/tree/main/example). 22 | 23 | | Timeline status | Package delivery tracking | Process timeline | 24 | | - | - | - | 25 | | [![timeline_status](https://raw.github.com/chulwoo-park/timelines/main/screenshots/timeline_status.gif)](https://chulwoo.dev/timelines/#/timeline_status) | [![package_delivery_tracking.gif](https://raw.github.com/chulwoo-park/timelines/main/screenshots/package_delivery_tracking.gif)](https://chulwoo.dev/timelines/#/package_delivery_tracking) | [![process_timeline.gif](https://raw.github.com/chulwoo-park/timelines/main/screenshots/process_timeline.gif)](https://chulwoo.dev/timelines/#/process_timeline) | 26 | 27 |

More examples
🚧 WIP 🚧

28 | 29 | # Features 30 | 31 | ### The [timeline](#timeline) and each [components](#components) are all WIDGET. 32 | 33 | * Common styles can be easily implemented with predefined components. 34 | * Vertical, horizontal direction. 35 | * Alternating contents. 36 | * Combination with Flutter widgets(Row, Column, CustomScrollView, etc). 37 | * Customize each range with themes. 38 | 39 | # Getting started 40 | 41 | - [Installation](#installation) 42 | - [Basic Usage](#basic-usage) 43 | - [Components](#components) 44 | - [Theme](#theme) 45 | - [Indicator](#indicator) 46 | - [Connector](#connector) 47 | - [TimelineNode](#timelinenode) 48 | - [TimelineTile](#timelinetile) 49 | - [Timeline](#timeline) 50 | - [TimelineTileBuilder](#timelinetilebuilder) 51 | 52 | ## Installation 53 | 54 | #### 1. Depend on it 55 | 56 | Add this to your package's pubspec.yaml file: 57 | ``` yaml 58 | dependencies: 59 | timelines: ^[latest_version] 60 | ``` 61 | 62 | #### 2. Install it 63 | You can install packages from the command line: 64 | 65 | with Flutter: 66 | ``` console 67 | $ flutter pub get 68 | ``` 69 | 70 | Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more. 71 | 72 | #### 3. Import it 73 | Now in your Dart code, you can use: 74 | ``` dart 75 | import 'package:timelines/timelines.dart'; 76 | ``` 77 | 78 | ## Basic Usage 79 | 80 | ``` dart 81 | @override 82 | Widget build(BuildContext context) { 83 | return Timeline.tileBuilder( 84 | builder: TimelineTileBuilder.fromStyle( 85 | contentsAlign: ContentsAlign.alternating, 86 | contentsBuilder: (context, index) => Padding( 87 | padding: const EdgeInsets.all(24.0), 88 | child: Text('Timeline Event $index'), 89 | ), 90 | itemCount: 10, 91 | ), 92 | ); 93 | } 94 | ``` 95 | 96 | Check the [Example](https://github.com/chulwoo-park/timelines/tree/main/example) or the [API reference](https://pub.dev/documentation/timelines/latest/) for more details. 97 | 98 | ## Components 99 | 100 | ### Theme 101 | 102 | Check out [Theme Demo](https://chulwoo.dev/timelines/#/theme) to see how the values inside TimelineTile work with the theme. 103 | 104 | To customize the timeline component with a theme, do the following: 105 | 106 | ``` dart 107 | TimelineTheme( 108 | data: TimelineThemeData(...), 109 | child: DotIndicator(...), 110 | ); 111 | ``` 112 | 113 | If you only want to change part of the parent theme, use `TimelineTheme.of(context)`: 114 | 115 | ``` dart 116 | TimelineTheme( 117 | data: TimelineThemeData.of(context).copyWith(...), 118 | child: DotIndicator(...), 119 | ); 120 | ``` 121 | 122 | If the component you want to customize is `Timeline` or `FixedTimeline`, this is also possible: 123 | 124 | ``` dart 125 | FixedTimeline( 126 | theme: TimelineThemeData(...), 127 | children: [...], 128 | ); 129 | ``` 130 | 131 | ### Indicator 132 | 133 | 134 | 135 | 136 | 139 | 149 | 150 | 151 | 152 | 155 | 156 | 157 | 158 | 159 | 162 | 163 | 164 |
ContainerIndicator
137 | ContainerIndicator 138 | 140 |
141 | ContainerIndicator(
142 |   child: Container(
143 |     width: 15.0,
144 |     height: 15.0,
145 |     color: Colors.blue,
146 |   ),
147 | )
148 |
DotIndicator
153 | DotIndicator 154 |
DotIndicator()
OutlinedDotIndicator
160 | OutlinedDotIndicator 161 |
OutlinedDotIndicator()
165 | 166 | ### Connector 167 | 168 | 169 | 170 | 171 | 174 | 181 | 182 | 183 | 184 | 187 | 194 | 195 | 196 | 197 | 200 | 215 | 216 |
SolidLineConnector
172 | SolidLineConnector 173 | 175 |
176 | SizedBox(
177 |   height: 20.0,
178 |   child: SolidLineConnector(),
179 | )
180 |
DashedLineConnector
185 | DashedLineConnector 186 | 188 |
189 | SizedBox(
190 |   height: 20.0,
191 |   child: DashedLineConnector(),
192 | )
193 |
DecoratedLineConnector
198 | DecoratedLineConnector 199 | 201 |
202 | SizedBox(
203 |   height: 20.0,
204 |   child: DecoratedLineConnector(
205 |     decoration: BoxDecoration(
206 |       gradient: LinearGradient(
207 |         begin: Alignment.topCenter,
208 |         end: Alignment.bottomCenter,
209 |         colors: [Colors.blue, Colors.lightBlueAccent[100]],
210 |       ),
211 |     ),
212 |   ),
213 | )
214 |
217 | 218 | 219 | ### TimelineNode 220 | 221 | Pure timeline UI component with no content. 222 | 223 | The TimelineNode contains an indicator and two connectors on both sides of the indicator: 224 | 225 | 226 | 227 | 228 | 231 | 238 | 239 | 240 | 241 | 244 | 261 | 262 |
Simple TimelineNode
229 | Simple TimelineNode 230 | 232 |
233 | SizedBox(
234 |   height: 50.0,
235 |   child: TimelineNode.simple(),
236 | )
237 |
Complex TimelineNode
242 | Complex TimelineNode 243 | 245 |
246 | SizedBox(
247 |   height: 80.0,
248 |   child: TimelineNode(
249 |     indicator: Card(
250 |       margin: EdgeInsets.zero,
251 |       child: Padding(
252 |         padding: EdgeInsets.all(8.0),
253 |         child: Text('Complex'),
254 |       ),
255 |     ),
256 |     startConnector: DashedLineConnector(),
257 |     endConnector: SolidLineConnector(),
258 |   ),
259 | )
260 |
263 | 264 | ### TimelineTile 265 | 266 | Displays content on both sides of the node: 267 | 268 | 269 | 270 | 271 | 274 | 294 | 295 |
TimelineTile
272 | TimelineTile 273 | 275 |
276 | TimelineTile(
277 |   oppositeContents: Padding(
278 |     padding: const EdgeInsets.all(8.0),
279 |     child: Text('opposite\ncontents'),
280 |   ),
281 |   contents: Card(
282 |     child: Container(
283 |       padding: EdgeInsets.all(8.0),
284 |       child: Text('contents'),
285 |     ),
286 |   ),
287 |   node: TimelineNode(
288 |     indicator: DotIndicator(),
289 |     startConnector: SolidLineConnector(),
290 |     endConnector: SolidLineConnector(),
291 |   ),
292 | )
293 |
296 | 297 | 298 | ### TimelineTileBuilder 299 | 300 | TimelineTileBuilder provides powerful build features. 301 | 302 | #### Connection 303 | 304 | Each tile draws only half of the line connecting the neighboring tiles. 305 | Using the `connected` constructor, lines connecting adjacent tiles can build as one index. 306 | 307 | 308 | 309 | 310 | 313 | 327 | 328 | 329 | 330 | 333 | 347 | 348 |
ConnectionDirection.before
311 | Connection direction before 312 | 314 |
315 | FixedTimeline.tileBuilder(
316 |   builder: TimelineTileBuilder.connectedFromStyle(
317 |     connectionDirection: ConnectionDirection.before,
318 |     connectorStyleBuilder: (context, index) {
319 |       return (index == 1) ? ConnectorStyle.dashedLine : ConnectorStyle.solidLine;
320 |     },
321 |     indicatorStyleBuilder: (context, index) => IndicatorStyle.dot,
322 |     itemExtent: 40.0,
323 |     itemCount: 3,
324 |   ),
325 | )
326 |
ConnectionDirection.after
331 | Connection direction after 332 | 334 |
335 | FixedTimeline.tileBuilder(
336 |   builder: TimelineTileBuilder.connectedFromStyle(
337 |     connectionDirection: ConnectionDirection.after,
338 |     connectorStyleBuilder: (context, index) {
339 |       return (index == 1) ? ConnectorStyle.dashedLine : ConnectorStyle.solidLine;
340 |     },
341 |     indicatorStyleBuilder: (context, index) => IndicatorStyle.dot,
342 |     itemExtent: 40.0,
343 |     itemCount: 3,
344 |   ),
345 | )
346 |
349 | 350 | 351 | #### ContentsAlign 352 | 353 | This value determines how the contents of the timeline will be built: 354 | 355 | 356 | 357 | 358 | 361 | 382 | 383 | 384 | 385 | 388 | 409 | 410 | 411 | 412 | 415 | 436 | 437 |
ContentsAlign.basic
359 | Basic contents align 360 | 362 |
363 | FixedTimeline.tileBuilder(
364 |   builder: TimelineTileBuilder.connectedFromStyle(
365 |     contentsAlign: ContentsAlign.basic,
366 |     oppositeContentsBuilder: (context, index) => Padding(
367 |       padding: const EdgeInsets.all(8.0),
368 |       child: Text('opposite\ncontents'),
369 |     ),
370 |     contentsBuilder: (context, index) => Card(
371 |       child: Padding(
372 |         padding: const EdgeInsets.all(8.0),
373 |         child: Text('Contents'),
374 |       ),
375 |     ),
376 |     connectorStyleBuilder: (context, index) => ConnectorStyle.solidLine,
377 |     indicatorStyleBuilder: (context, index) => IndicatorStyle.dot,
378 |     itemCount: 3,
379 |   ),
380 | )
381 |
ContentsAlign.reverse
386 | Reverse contents align 387 | 389 |
390 | FixedTimeline.tileBuilder(
391 |   builder: TimelineTileBuilder.connectedFromStyle(
392 |     contentsAlign: ContentsAlign.reverse,
393 |     oppositeContentsBuilder: (context, index) => Padding(
394 |       padding: const EdgeInsets.all(8.0),
395 |       child: Text('opposite\ncontents'),
396 |     ),
397 |     contentsBuilder: (context, index) => Card(
398 |       child: Padding(
399 |         padding: const EdgeInsets.all(8.0),
400 |         child: Text('Contents'),
401 |       ),
402 |     ),
403 |     connectorStyleBuilder: (context, index) => ConnectorStyle.solidLine,
404 |     indicatorStyleBuilder: (context, index) => IndicatorStyle.dot,
405 |     itemCount: 3,
406 |   ),
407 | )
408 |
ContentsAlign.alternating
413 | Alternating contents align 414 | 416 |
417 | FixedTimeline.tileBuilder(
418 |   builder: TimelineTileBuilder.connectedFromStyle(
419 |     contentsAlign: ContentsAlign.alternating,
420 |     oppositeContentsBuilder: (context, index) => Padding(
421 |       padding: const EdgeInsets.all(8.0),
422 |       child: Text('opposite\ncontents'),
423 |     ),
424 |     contentsBuilder: (context, index) => Card(
425 |       child: Padding(
426 |         padding: const EdgeInsets.all(8.0),
427 |         child: Text('Contents'),
428 |       ),
429 |     ),
430 |     connectorStyleBuilder: (context, index) => ConnectorStyle.solidLine,
431 |     indicatorStyleBuilder: (context, index) => IndicatorStyle.dot,
432 |     itemCount: 3,
433 |   ),
434 | )
435 |
438 | 439 | 440 | ### Timeline 441 | 442 | The timeline component has two widgets, `Timeline` similar to ScrollView and `FixedTimeline` similar to Flex. 443 | 444 | Also their constructors are similar to ScrollView and Flex. 445 | 446 | The main difference is that they has TimelineTheme as an ancestor. 447 | 448 | The `tileBuilder` constructor provides more powerful features using [TimelineTileBuilder](https://pub.dev/documentation/timelines/latest/timelines/TimelineTileBuilder-class.html). 449 | 450 | If you don't need TimelineTileBuilder, you can use other flutter widgets like ListView, Column, Row, etc. 451 | 452 | Even if you use the flutter widget, you can use TimelineTheme. 453 | 454 | 455 | # Documentation 456 | 457 | See full [documentation](https://pub.dev/documentation/timelines/latest/) 458 | 459 | # Changelog 460 | 461 | See [CHANGELOG.md](https://github.com/chulwoo-park/timelines/blob/main/CHANGELOG.md). 462 | 463 | # Code of conduct 464 | 465 | See [CODE_OF_CONDUCT.md](https://github.com/chulwoo-park/timelines/blob/main/CODE_OF_CONDUCT.md). 466 | 467 | # License 468 | 469 | [MIT](https://github.com/chulwoo-park/timelines/blob/main/LICENSE) 470 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | iinclude: package:pedantic/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | - prefer_relative_imports 6 | - sort_constructors_first -------------------------------------------------------------------------------- /design/feature_graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/design/feature_graphic.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .classpath 21 | .project 22 | .settings/ 23 | .vscode/ 24 | 25 | # Flutter repo-specific 26 | /bin/cache/ 27 | /bin/internal/bootstrap.bat 28 | /bin/internal/bootstrap.sh 29 | /bin/mingit/ 30 | /dev/benchmarks/mega_gallery/ 31 | /dev/bots/.recipe_deps 32 | /dev/bots/android_tools/ 33 | /dev/devicelab/ABresults*.json 34 | /dev/docs/doc/ 35 | /dev/docs/flutter.docs.zip 36 | /dev/docs/lib/ 37 | /dev/docs/pubspec.yaml 38 | /dev/integration_tests/**/xcuserdata 39 | /dev/integration_tests/**/Pods 40 | /packages/flutter/coverage/ 41 | version 42 | analysis_benchmark.json 43 | 44 | # packages file containing multi-root paths 45 | .packages.generated 46 | 47 | # Flutter/Dart/Pub related 48 | **/doc/api/ 49 | .dart_tool/ 50 | .flutter-plugins 51 | .flutter-plugins-dependencies 52 | **/generated_plugin_registrant.dart 53 | .packages 54 | .pub-cache/ 55 | .pub/ 56 | build/ 57 | flutter_*.png 58 | linked_*.ds 59 | unlinked.ds 60 | unlinked_spec.ds 61 | 62 | # Android related 63 | **/android/**/gradle-wrapper.jar 64 | **/android/.gradle 65 | **/android/captures/ 66 | **/android/gradlew 67 | **/android/gradlew.bat 68 | **/android/local.properties 69 | **/android/**/GeneratedPluginRegistrant.java 70 | **/android/key.properties 71 | *.jks 72 | 73 | # iOS/XCode related 74 | **/ios/**/*.mode1v3 75 | **/ios/**/*.mode2v3 76 | **/ios/**/*.moved-aside 77 | **/ios/**/*.pbxuser 78 | **/ios/**/*.perspectivev3 79 | **/ios/**/*sync/ 80 | **/ios/**/.sconsign.dblite 81 | **/ios/**/.tags* 82 | **/ios/**/.vagrant/ 83 | **/ios/**/DerivedData/ 84 | **/ios/**/Icon? 85 | **/ios/**/Pods/ 86 | **/ios/**/.symlinks/ 87 | **/ios/**/profile 88 | **/ios/**/xcuserdata 89 | **/ios/.generated/ 90 | **/ios/Flutter/.last_build_id 91 | **/ios/Flutter/App.framework 92 | **/ios/Flutter/Flutter.framework 93 | **/ios/Flutter/Flutter.podspec 94 | **/ios/Flutter/Generated.xcconfig 95 | **/ios/Flutter/app.flx 96 | **/ios/Flutter/app.zip 97 | **/ios/Flutter/flutter_assets/ 98 | **/ios/Flutter/flutter_export_environment.sh 99 | **/ios/ServiceDefinitions.json 100 | **/ios/Runner/GeneratedPluginRegistrant.* 101 | 102 | # macOS 103 | **/macos/Flutter/GeneratedPluginRegistrant.swift 104 | **/macos/Flutter/Flutter-Debug.xcconfig 105 | **/macos/Flutter/Flutter-Release.xcconfig 106 | **/macos/Flutter/Flutter-Profile.xcconfig 107 | 108 | # Coverage 109 | coverage/ 110 | 111 | # Symbols 112 | app.*.symbols 113 | 114 | # Exceptions to above rules. 115 | !**/ios/**/default.mode1v3 116 | !**/ios/**/default.mode2v3 117 | !**/ios/**/default.pbxuser 118 | !**/ios/**/default.perspectivev3 119 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 120 | !/dev/ci/**/Gemfile.lock -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 198df796aa80073ef22bdf249e614e2ff33c6895 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/EXAMPLE.md: -------------------------------------------------------------------------------- 1 | 2 | This example shows a simple timeline that has text contents alternating: 3 | 4 | ![timeline_status](https://raw.github.com/chulwoo-park/timelines/main/screenshots/example.png) 5 | 6 | 7 | ``` dart 8 | import 'package:flutter/material.dart'; 9 | import 'package:timelines/timelines.dart'; 10 | 11 | void main() { 12 | runApp(MyApp()); 13 | } 14 | 15 | class MyApp extends StatelessWidget { 16 | @override 17 | Widget build(BuildContext context) { 18 | return MaterialApp( 19 | title: 'Timelines Demo', 20 | theme: ThemeData( 21 | primarySwatch: Colors.blue, 22 | ), 23 | home: MyHomePage(title: 'Timelines Demo Home Page'), 24 | ); 25 | } 26 | } 27 | 28 | class MyHomePage extends StatefulWidget { 29 | MyHomePage({Key key, this.title}) : super(key: key); 30 | 31 | final String title; 32 | 33 | @override 34 | _MyHomePageState createState() => _MyHomePageState(); 35 | } 36 | 37 | class _MyHomePageState extends State { 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return Scaffold( 42 | appBar: AppBar( 43 | title: Text(widget.title), 44 | ), 45 | body: Timeline.tileBuilder( 46 | builder: TimelineTileBuilder.fromStyle( 47 | contentsAlign: ContentsAlign.alternating, 48 | contentsBuilder: (context, index) => Padding( 49 | padding: const EdgeInsets.all(24.0), 50 | child: Text('Timeline Event $index'), 51 | ), 52 | itemCount: 10, 53 | ), 54 | ), 55 | ); 56 | } 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Check it out on the [web](https://chulwoo.dev/timelines/) or look at the [source code](https://github.com/chulwoo-park/timelines/tree/main/example). 4 | 5 | | Timeline status | Package delivery tracking | Process timeline | 6 | | - | - | - | 7 | | [![timeline_status](https://raw.github.com/chulwoo-park/timelines/main/screenshots/timeline_status.gif)](https://chulwoo.dev/timelines/#/timeline_status) | [![package_delivery_tracking.gif](https://raw.github.com/chulwoo-park/timelines/main/screenshots/package_delivery_tracking.gif)](https://chulwoo.dev/timelines/#/package_delivery_tracking) | [![process_timeline.gif](https://raw.github.com/chulwoo-park/timelines/main/screenshots/process_timeline.gif)](https://chulwoo.dev/timelines/#/process_timeline) | 8 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 29 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "dev.chulwoo.timelines.example" 42 | minSdkVersion 16 43 | targetSdkVersion 29 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | } 47 | 48 | buildTypes { 49 | release { 50 | // TODO: Add your own signing config for the release build. 51 | // Signing with the debug keys for now, so `flutter run --release` works. 52 | signingConfig signingConfigs.debug 53 | } 54 | } 55 | } 56 | 57 | flutter { 58 | source '../..' 59 | } 60 | 61 | dependencies { 62 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 63 | } 64 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 13 | 17 | 21 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/dev/chulwoo/timelines/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.chulwoo.timelines.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/assets/images/package_delivery_tracking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/assets/images/package_delivery_tracking.png -------------------------------------------------------------------------------- /example/assets/images/process_timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/assets/images/process_timeline.png -------------------------------------------------------------------------------- /example/assets/images/process_timeline/status1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/assets/images/process_timeline/status1.png -------------------------------------------------------------------------------- /example/assets/images/process_timeline/status2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/assets/images/process_timeline/status2.png -------------------------------------------------------------------------------- /example/assets/images/process_timeline/status3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/assets/images/process_timeline/status3.png -------------------------------------------------------------------------------- /example/assets/images/process_timeline/status4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/assets/images/process_timeline/status4.png -------------------------------------------------------------------------------- /example/assets/images/process_timeline/status5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/assets/images/process_timeline/status5.png -------------------------------------------------------------------------------- /example/assets/images/timeline_status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/assets/images/timeline_status.png -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/component_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:timelines/timelines.dart'; 3 | 4 | import 'widget.dart'; 5 | 6 | class ComponentPage extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | final children = [ 10 | _ComponentRow( 11 | name: 'Dot\nIndicator', 12 | item: DotIndicator(), 13 | ), 14 | _ComponentRow( 15 | name: 'Outlined dot\nIndicator', 16 | item: OutlinedDotIndicator(), 17 | ), 18 | _ComponentRow( 19 | name: 'Container\nIndicator', 20 | item: ContainerIndicator( 21 | child: Container( 22 | width: 15.0, 23 | height: 15.0, 24 | color: Colors.blue, 25 | ), 26 | ), 27 | ), 28 | _ComponentRow( 29 | name: 'Solid line\nConnector', 30 | item: SizedBox( 31 | height: 20.0, 32 | child: SolidLineConnector(), 33 | ), 34 | ), 35 | _ComponentRow( 36 | name: 'Dashed line\nConnector', 37 | item: SizedBox( 38 | height: 20.0, 39 | child: DashedLineConnector(), 40 | ), 41 | ), 42 | _ComponentRow( 43 | name: 'Decorated line\nConnector', 44 | item: SizedBox( 45 | height: 20.0, 46 | child: DecoratedLineConnector( 47 | decoration: BoxDecoration( 48 | gradient: LinearGradient( 49 | begin: Alignment.topCenter, 50 | end: Alignment.bottomCenter, 51 | colors: [Colors.blue, Colors.lightBlueAccent.shade100], 52 | ), 53 | ), 54 | ), 55 | ), 56 | ), 57 | _ComponentRow( 58 | name: 'Simple TimelineNode', 59 | item: SizedBox( 60 | height: 50.0, 61 | child: TimelineNode.simple(), 62 | ), 63 | ), 64 | _ComponentRow( 65 | name: 'Complex TimelineNode', 66 | item: SizedBox( 67 | height: 80.0, 68 | child: TimelineNode( 69 | indicator: Card( 70 | margin: EdgeInsets.zero, 71 | child: Padding( 72 | padding: EdgeInsets.all(8.0), 73 | child: Text('Complex'), 74 | ), 75 | ), 76 | startConnector: DashedLineConnector(), 77 | endConnector: SolidLineConnector(), 78 | ), 79 | ), 80 | ), 81 | _ComponentRow( 82 | name: 'TimelineTile', 83 | item: TimelineTile( 84 | oppositeContents: Padding( 85 | padding: const EdgeInsets.all(8.0), 86 | child: Text('opposite\ncontents'), 87 | ), 88 | contents: Card( 89 | child: Container( 90 | padding: EdgeInsets.all(8.0), 91 | child: Text('contents'), 92 | ), 93 | ), 94 | node: TimelineNode( 95 | indicator: DotIndicator(), 96 | startConnector: SolidLineConnector(), 97 | endConnector: SolidLineConnector(), 98 | ), 99 | ), 100 | ), 101 | _ComponentRow( 102 | name: 'ConnectionDirection.before', 103 | item: Padding( 104 | padding: const EdgeInsets.only(bottom: 8.0), 105 | child: FixedTimeline.tileBuilder( 106 | builder: TimelineTileBuilder.connectedFromStyle( 107 | connectionDirection: ConnectionDirection.before, 108 | connectorStyleBuilder: (context, index) { 109 | return (index == 1) 110 | ? ConnectorStyle.dashedLine 111 | : ConnectorStyle.solidLine; 112 | }, 113 | indicatorStyleBuilder: (context, index) => IndicatorStyle.dot, 114 | itemExtent: 40.0, 115 | itemCount: 3, 116 | ), 117 | ), 118 | ), 119 | ), 120 | _ComponentRow( 121 | name: 'ConnectionDirection.after', 122 | item: Padding( 123 | padding: const EdgeInsets.all(8.0), 124 | child: FixedTimeline.tileBuilder( 125 | builder: TimelineTileBuilder.connectedFromStyle( 126 | connectionDirection: ConnectionDirection.after, 127 | connectorStyleBuilder: (context, index) { 128 | return (index == 1) 129 | ? ConnectorStyle.dashedLine 130 | : ConnectorStyle.solidLine; 131 | }, 132 | indicatorStyleBuilder: (context, index) => IndicatorStyle.dot, 133 | itemExtent: 40.0, 134 | itemCount: 3, 135 | ), 136 | ), 137 | ), 138 | ), 139 | _ComponentRow( 140 | name: 'ContentsAlign.basic', 141 | item: Padding( 142 | padding: const EdgeInsets.all(8.0), 143 | child: FixedTimeline.tileBuilder( 144 | builder: TimelineTileBuilder.connectedFromStyle( 145 | contentsAlign: ContentsAlign.basic, 146 | oppositeContentsBuilder: (context, index) => Padding( 147 | padding: const EdgeInsets.all(8.0), 148 | child: Text('opposite\ncontents'), 149 | ), 150 | contentsBuilder: (context, index) => Card( 151 | child: Padding( 152 | padding: const EdgeInsets.all(8.0), 153 | child: Text('Contents'), 154 | ), 155 | ), 156 | connectorStyleBuilder: (context, index) => 157 | ConnectorStyle.solidLine, 158 | indicatorStyleBuilder: (context, index) => IndicatorStyle.dot, 159 | itemCount: 3, 160 | ), 161 | ), 162 | ), 163 | ), 164 | _ComponentRow( 165 | name: 'ContentsAlign.reverse', 166 | item: Padding( 167 | padding: const EdgeInsets.all(8.0), 168 | child: FixedTimeline.tileBuilder( 169 | builder: TimelineTileBuilder.connectedFromStyle( 170 | contentsAlign: ContentsAlign.reverse, 171 | oppositeContentsBuilder: (context, index) => Padding( 172 | padding: const EdgeInsets.all(8.0), 173 | child: Text('opposite\ncontents'), 174 | ), 175 | contentsBuilder: (context, index) => Card( 176 | child: Padding( 177 | padding: const EdgeInsets.all(8.0), 178 | child: Text('Contents'), 179 | ), 180 | ), 181 | connectorStyleBuilder: (context, index) => 182 | ConnectorStyle.solidLine, 183 | indicatorStyleBuilder: (context, index) => IndicatorStyle.dot, 184 | itemCount: 3, 185 | ), 186 | ), 187 | ), 188 | ), 189 | _ComponentRow( 190 | name: 'ContentsAlign.alternating', 191 | item: FixedTimeline.tileBuilder( 192 | builder: TimelineTileBuilder.connectedFromStyle( 193 | contentsAlign: ContentsAlign.alternating, 194 | oppositeContentsBuilder: (context, index) => Padding( 195 | padding: const EdgeInsets.all(8.0), 196 | child: Text('opposite\ncontents'), 197 | ), 198 | contentsBuilder: (context, index) => Card( 199 | child: Padding( 200 | padding: const EdgeInsets.all(8.0), 201 | child: Text('Contents'), 202 | ), 203 | ), 204 | connectorStyleBuilder: (context, index) => ConnectorStyle.solidLine, 205 | indicatorStyleBuilder: (context, index) => IndicatorStyle.dot, 206 | itemCount: 3, 207 | ), 208 | ), 209 | ), 210 | _ComponentRow( 211 | name: 'Horizontal\nTimeline', 212 | item: SizedBox( 213 | height: 150, 214 | child: Timeline.tileBuilder( 215 | // shrinkWrap: true, 216 | scrollDirection: Axis.horizontal, 217 | builder: TimelineTileBuilder.fromStyle( 218 | contentsBuilder: (context, index) => Card( 219 | child: Padding( 220 | padding: const EdgeInsets.all(8.0), 221 | child: Text('Contents'), 222 | ), 223 | ), 224 | oppositeContentsBuilder: (context, index) => Padding( 225 | padding: const EdgeInsets.all(8.0), 226 | child: Text('opposite\ncontents'), 227 | ), 228 | itemCount: 20, 229 | ), 230 | ), 231 | ), 232 | ), 233 | _ComponentRow( 234 | name: 'Styled node\nHorizontal\nTimeline', 235 | item: SizedBox( 236 | height: 150, 237 | child: Timeline.tileBuilder( 238 | // shrinkWrap: true, 239 | scrollDirection: Axis.horizontal, 240 | builder: TimelineTileBuilder.fromStyle( 241 | contentsBuilder: (context, index) => Card( 242 | child: Padding( 243 | padding: const EdgeInsets.all(8.0), 244 | child: Text('Contents'), 245 | ), 246 | ), 247 | oppositeContentsBuilder: (context, index) => Padding( 248 | padding: const EdgeInsets.all(8.0), 249 | child: Text('opposite\ncontents'), 250 | ), 251 | indicatorStyle: IndicatorStyle.outlined, 252 | connectorStyle: ConnectorStyle.dashedLine, 253 | itemCount: 20, 254 | ), 255 | ), 256 | ), 257 | ), 258 | _ComponentRow( 259 | name: 'Reverse\nHorizontal\nTimeline', 260 | item: SizedBox( 261 | height: 150, 262 | child: Timeline.tileBuilder( 263 | // shrinkWrap: true, 264 | scrollDirection: Axis.horizontal, 265 | builder: TimelineTileBuilder.fromStyle( 266 | contentsBuilder: (context, index) => Card( 267 | child: Padding( 268 | padding: const EdgeInsets.all(8.0), 269 | child: Text('Contents'), 270 | ), 271 | ), 272 | oppositeContentsBuilder: (context, index) => Padding( 273 | padding: const EdgeInsets.all(8.0), 274 | child: Text('opposite\ncontents'), 275 | ), 276 | contentsAlign: ContentsAlign.reverse, 277 | itemCount: 20, 278 | ), 279 | ), 280 | ), 281 | ), 282 | _ComponentRow( 283 | name: 'Alternating\nHorizontal\nTimeline', 284 | item: SizedBox( 285 | height: 150, 286 | child: Timeline.tileBuilder( 287 | // shrinkWrap: true, 288 | scrollDirection: Axis.horizontal, 289 | builder: TimelineTileBuilder.fromStyle( 290 | contentsBuilder: (context, index) => Card( 291 | child: Padding( 292 | padding: const EdgeInsets.all(8.0), 293 | child: Text('Contents'), 294 | ), 295 | ), 296 | oppositeContentsBuilder: (context, index) => Padding( 297 | padding: const EdgeInsets.all(8.0), 298 | child: Text('opposite\ncontents'), 299 | ), 300 | contentsAlign: ContentsAlign.alternating, 301 | itemCount: 20, 302 | ), 303 | ), 304 | ), 305 | ), 306 | _ComponentRow( 307 | name: 'Vertical\nTimeline', 308 | item: SizedBox( 309 | height: 500, 310 | child: Timeline.tileBuilder( 311 | builder: TimelineTileBuilder.fromStyle( 312 | contentsBuilder: (context, index) => Card( 313 | child: Padding( 314 | padding: const EdgeInsets.all(8.0), 315 | child: Text('Contents'), 316 | ), 317 | ), 318 | oppositeContentsBuilder: (context, index) => Padding( 319 | padding: const EdgeInsets.all(8.0), 320 | child: Text('opposite\ncontents'), 321 | ), 322 | contentsAlign: ContentsAlign.alternating, 323 | indicatorStyle: IndicatorStyle.outlined, 324 | connectorStyle: ConnectorStyle.dashedLine, 325 | itemCount: 10, 326 | ), 327 | ), 328 | ), 329 | ), 330 | ]; 331 | 332 | return Scaffold( 333 | appBar: TitleAppBar('Components'), 334 | body: SingleChildScrollView( 335 | scrollDirection: Axis.vertical, 336 | child: Table( 337 | children: children, 338 | columnWidths: { 339 | 0: FlexColumnWidth(1), 340 | 1: FlexColumnWidth(2), 341 | 2: FlexColumnWidth(0.3), 342 | }, 343 | ), 344 | ), 345 | ); 346 | } 347 | } 348 | 349 | class _ComponentRow extends TableRow { 350 | _ComponentRow({ 351 | required String name, 352 | required Widget item, 353 | }) : super( 354 | children: [ 355 | _ComponentName(name), 356 | _ComponentItem(child: item), 357 | ], 358 | ); 359 | } 360 | 361 | class _ComponentItem extends StatelessWidget { 362 | const _ComponentItem({ 363 | Key? key, 364 | required this.child, 365 | }) : super(key: key); 366 | 367 | final Widget child; 368 | 369 | @override 370 | Widget build(BuildContext context) { 371 | return Container( 372 | constraints: BoxConstraints( 373 | minHeight: 65.0, 374 | ), 375 | child: Center( 376 | child: child, 377 | ), 378 | ); 379 | } 380 | } 381 | 382 | class _ComponentName extends StatelessWidget { 383 | const _ComponentName( 384 | this.name, { 385 | Key? key, 386 | }) : assert(name.length > 0), 387 | super(key: key); 388 | 389 | final String name; 390 | 391 | @override 392 | Widget build(BuildContext context) { 393 | return _ComponentItem( 394 | child: Padding( 395 | padding: const EdgeInsets.all(8.0), 396 | child: Align( 397 | alignment: Alignment.centerLeft, 398 | child: FittedBox( 399 | child: Text(name), 400 | ), 401 | ), 402 | ), 403 | ); 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/rendering.dart'; 4 | import 'package:timelines/timelines.dart'; 5 | 6 | import 'component_page.dart'; 7 | import 'showcase/package_delivery_tracking.dart'; 8 | import 'showcase/process_timeline.dart'; 9 | import 'showcase/timeline_status.dart'; 10 | import 'showcase_page.dart'; 11 | import 'theme_page.dart'; 12 | import 'widget.dart'; 13 | 14 | void main() { 15 | runApp(MyApp()); 16 | } 17 | 18 | class MyApp extends StatelessWidget { 19 | @override 20 | Widget build(BuildContext context) { 21 | return MaterialApp( 22 | title: 'Timelines Demo', 23 | theme: ThemeData.light(), 24 | darkTheme: ThemeData.dark(), 25 | onGenerateRoute: (settings) { 26 | String? path = Uri.tryParse(settings.name!)?.path; 27 | Widget child; 28 | switch (path) { 29 | case '/theme': 30 | child = ThemePage(); 31 | break; 32 | case '/timeline_status': 33 | child = TimelineStatusPage(); 34 | break; 35 | case '/package_delivery_tracking': 36 | child = PackageDeliveryTrackingPage(); 37 | break; 38 | case '/process_timeline': 39 | child = ProcessTimelinePage(); 40 | break; 41 | default: 42 | child = ExamplePage(); 43 | } 44 | 45 | return MaterialPageRoute(builder: (context) => HomePage(child: child)); 46 | }, 47 | initialRoute: '/', 48 | ); 49 | } 50 | } 51 | 52 | class HomePage extends StatefulWidget { 53 | HomePage({ 54 | Key? key, 55 | required this.child, 56 | }) : super(key: key); 57 | 58 | final Widget child; 59 | 60 | @override 61 | _HomePageState createState() => _HomePageState(); 62 | } 63 | 64 | class _HomePageState extends State { 65 | final _navigatorKey = GlobalKey(); 66 | 67 | @override 68 | void didUpdateWidget(covariant HomePage oldWidget) { 69 | super.didUpdateWidget(oldWidget); 70 | if (oldWidget.child != widget.child) { 71 | setState(() {}); 72 | } 73 | } 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return WillPopScope( 78 | onWillPop: () async { 79 | if (_navigatorKey.currentState?.canPop() ?? false) { 80 | _navigatorKey.currentState?.maybePop(); 81 | return false; 82 | } else { 83 | return true; 84 | } 85 | }, 86 | child: Column( 87 | children: [ 88 | Expanded( 89 | child: Navigator( 90 | key: _navigatorKey, 91 | onGenerateRoute: (settings) => MaterialPageRoute( 92 | builder: (context) => widget.child, 93 | ), 94 | ), 95 | ), 96 | if (kIsWeb) WebAlert() 97 | ], 98 | ), 99 | ); 100 | } 101 | } 102 | 103 | class WebAlert extends StatelessWidget { 104 | @override 105 | Widget build(BuildContext context) { 106 | return SizedBox( 107 | height: 80.0, 108 | child: Material( 109 | child: Center( 110 | child: Text( 111 | 'You are using the web version now.\nSome UI can be broken.', 112 | textAlign: TextAlign.center, 113 | ), 114 | ), 115 | ), 116 | ); 117 | } 118 | } 119 | 120 | class ExamplePage extends StatelessWidget { 121 | @override 122 | Widget build(BuildContext context) { 123 | return TimelineTheme( 124 | data: TimelineThemeData( 125 | indicatorTheme: IndicatorThemeData(size: 15.0), 126 | ), 127 | child: Scaffold( 128 | appBar: AppBar( 129 | title: Text('Timelines Example'), 130 | ), 131 | body: ListView( 132 | padding: EdgeInsets.all(20.0), 133 | children: [ 134 | _NavigationCard( 135 | name: 'Components', 136 | navigationBuilder: () => ComponentPage(), 137 | ), 138 | _NavigationCard( 139 | name: 'Theme', 140 | navigationBuilder: () => ThemePage(), 141 | ), 142 | _NavigationCard( 143 | name: 'Showcase', 144 | navigationBuilder: () => ShowcasePage(), 145 | ), 146 | ], 147 | ), 148 | ), 149 | ); 150 | } 151 | } 152 | 153 | class _NavigationCard extends StatelessWidget { 154 | const _NavigationCard({ 155 | Key? key, 156 | required this.name, 157 | this.navigationBuilder, 158 | }) : super(key: key); 159 | 160 | final String name; 161 | final NavigateWidgetBuilder? navigationBuilder; 162 | 163 | @override 164 | Widget build(BuildContext context) { 165 | return Center( 166 | child: NavigationCard( 167 | margin: EdgeInsets.symmetric( 168 | horizontal: 20.0, 169 | vertical: 10.0, 170 | ), 171 | borderRadius: BorderRadius.circular(8), 172 | navigationBuilder: navigationBuilder, 173 | child: Padding( 174 | padding: const EdgeInsets.all(20.0), 175 | child: Row( 176 | children: [ 177 | Expanded( 178 | child: Text(name), 179 | ), 180 | Icon(Icons.chevron_right), 181 | ], 182 | ), 183 | ), 184 | ), 185 | ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /example/lib/showcase/package_delivery_tracking.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:timelines/timelines.dart'; 3 | 4 | import '../widget.dart'; 5 | 6 | const kTileHeight = 50.0; 7 | 8 | class PackageDeliveryTrackingPage extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: TitleAppBar('Package Delivery Tracking'), 13 | body: ListView.builder( 14 | itemBuilder: (context, index) { 15 | final data = _data(index + 1); 16 | return Center( 17 | child: Container( 18 | width: 360.0, 19 | child: Card( 20 | margin: EdgeInsets.all(20.0), 21 | child: Column( 22 | mainAxisSize: MainAxisSize.min, 23 | children: [ 24 | Padding( 25 | padding: const EdgeInsets.all(20.0), 26 | child: _OrderTitle( 27 | orderInfo: data, 28 | ), 29 | ), 30 | Divider(height: 1.0), 31 | _DeliveryProcesses(processes: data.deliveryProcesses), 32 | Divider(height: 1.0), 33 | Padding( 34 | padding: const EdgeInsets.all(20.0), 35 | child: _OnTimeBar(driver: data.driverInfo), 36 | ), 37 | ], 38 | ), 39 | ), 40 | ), 41 | ); 42 | }, 43 | ), 44 | ); 45 | } 46 | } 47 | 48 | class _OrderTitle extends StatelessWidget { 49 | const _OrderTitle({ 50 | Key? key, 51 | required this.orderInfo, 52 | }) : super(key: key); 53 | 54 | final _OrderInfo orderInfo; 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return Row( 59 | children: [ 60 | Text( 61 | 'Delivery #${orderInfo.id}', 62 | style: TextStyle( 63 | fontWeight: FontWeight.bold, 64 | ), 65 | ), 66 | Spacer(), 67 | Text( 68 | '${orderInfo.date.day}/${orderInfo.date.month}/${orderInfo.date.year}', 69 | style: TextStyle( 70 | color: Color(0xffb6b2b2), 71 | ), 72 | ), 73 | ], 74 | ); 75 | } 76 | } 77 | 78 | class _InnerTimeline extends StatelessWidget { 79 | const _InnerTimeline({ 80 | required this.messages, 81 | }); 82 | 83 | final List<_DeliveryMessage> messages; 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | bool isEdgeIndex(int index) { 88 | return index == 0 || index == messages.length + 1; 89 | } 90 | 91 | return Padding( 92 | padding: const EdgeInsets.symmetric(vertical: 8.0), 93 | child: FixedTimeline.tileBuilder( 94 | theme: TimelineTheme.of(context).copyWith( 95 | nodePosition: 0, 96 | connectorTheme: TimelineTheme.of(context).connectorTheme.copyWith( 97 | thickness: 1.0, 98 | ), 99 | indicatorTheme: TimelineTheme.of(context).indicatorTheme.copyWith( 100 | size: 10.0, 101 | position: 0.5, 102 | ), 103 | ), 104 | builder: TimelineTileBuilder( 105 | indicatorBuilder: (_, index) => 106 | !isEdgeIndex(index) ? Indicator.outlined(borderWidth: 1.0) : null, 107 | startConnectorBuilder: (_, index) => Connector.solidLine(), 108 | endConnectorBuilder: (_, index) => Connector.solidLine(), 109 | contentsBuilder: (_, index) { 110 | if (isEdgeIndex(index)) { 111 | return null; 112 | } 113 | 114 | return Padding( 115 | padding: EdgeInsets.only(left: 8.0), 116 | child: Text(messages[index - 1].toString()), 117 | ); 118 | }, 119 | itemExtentBuilder: (_, index) => isEdgeIndex(index) ? 10.0 : 30.0, 120 | nodeItemOverlapBuilder: (_, index) => 121 | isEdgeIndex(index) ? true : null, 122 | itemCount: messages.length + 2, 123 | ), 124 | ), 125 | ); 126 | } 127 | } 128 | 129 | class _DeliveryProcesses extends StatelessWidget { 130 | const _DeliveryProcesses({Key? key, required this.processes}) 131 | : super(key: key); 132 | 133 | final List<_DeliveryProcess> processes; 134 | @override 135 | Widget build(BuildContext context) { 136 | return DefaultTextStyle( 137 | style: TextStyle( 138 | color: Color(0xff9b9b9b), 139 | fontSize: 12.5, 140 | ), 141 | child: Padding( 142 | padding: const EdgeInsets.all(20.0), 143 | child: FixedTimeline.tileBuilder( 144 | theme: TimelineThemeData( 145 | nodePosition: 0, 146 | color: Color(0xff989898), 147 | indicatorTheme: IndicatorThemeData( 148 | position: 0, 149 | size: 20.0, 150 | ), 151 | connectorTheme: ConnectorThemeData( 152 | thickness: 2.5, 153 | ), 154 | ), 155 | builder: TimelineTileBuilder.connected( 156 | connectionDirection: ConnectionDirection.before, 157 | itemCount: processes.length, 158 | contentsBuilder: (_, index) { 159 | if (processes[index].isCompleted) return null; 160 | 161 | return Padding( 162 | padding: EdgeInsets.only(left: 8.0), 163 | child: Column( 164 | crossAxisAlignment: CrossAxisAlignment.start, 165 | mainAxisSize: MainAxisSize.min, 166 | children: [ 167 | Text( 168 | processes[index].name, 169 | style: DefaultTextStyle.of(context).style.copyWith( 170 | fontSize: 18.0, 171 | ), 172 | ), 173 | _InnerTimeline(messages: processes[index].messages), 174 | ], 175 | ), 176 | ); 177 | }, 178 | indicatorBuilder: (_, index) { 179 | if (processes[index].isCompleted) { 180 | return DotIndicator( 181 | color: Color(0xff66c97f), 182 | child: Icon( 183 | Icons.check, 184 | color: Colors.white, 185 | size: 12.0, 186 | ), 187 | ); 188 | } else { 189 | return OutlinedDotIndicator( 190 | borderWidth: 2.5, 191 | ); 192 | } 193 | }, 194 | connectorBuilder: (_, index, ___) => SolidLineConnector( 195 | color: processes[index].isCompleted ? Color(0xff66c97f) : null, 196 | ), 197 | ), 198 | ), 199 | ), 200 | ); 201 | } 202 | } 203 | 204 | class _OnTimeBar extends StatelessWidget { 205 | const _OnTimeBar({Key? key, required this.driver}) : super(key: key); 206 | 207 | final _DriverInfo driver; 208 | 209 | @override 210 | Widget build(BuildContext context) { 211 | return Row( 212 | children: [ 213 | MaterialButton( 214 | onPressed: () { 215 | ScaffoldMessenger.of(context).showSnackBar( 216 | SnackBar( 217 | content: Text('On-time!'), 218 | ), 219 | ); 220 | }, 221 | elevation: 0, 222 | shape: StadiumBorder(), 223 | color: Color(0xff66c97f), 224 | textColor: Colors.white, 225 | child: Text('On-time'), 226 | ), 227 | Spacer(), 228 | Text( 229 | 'Driver\n${driver.name}', 230 | textAlign: TextAlign.center, 231 | ), 232 | SizedBox(width: 12.0), 233 | Container( 234 | width: 40.0, 235 | height: 40.0, 236 | decoration: BoxDecoration( 237 | shape: BoxShape.circle, 238 | image: DecorationImage( 239 | fit: BoxFit.fitWidth, 240 | image: NetworkImage( 241 | driver.thumbnailUrl, 242 | ), 243 | ), 244 | ), 245 | ), 246 | ], 247 | ); 248 | } 249 | } 250 | 251 | _OrderInfo _data(int id) => _OrderInfo( 252 | id: id, 253 | date: DateTime.now(), 254 | driverInfo: _DriverInfo( 255 | name: 'Philipe', 256 | thumbnailUrl: 257 | 'https://i.pinimg.com/originals/08/45/81/084581e3155d339376bf1d0e17979dc6.jpg', 258 | ), 259 | deliveryProcesses: [ 260 | _DeliveryProcess( 261 | 'Package Process', 262 | messages: [ 263 | _DeliveryMessage('8:30am', 'Package received by driver'), 264 | _DeliveryMessage('11:30am', 'Reached halfway mark'), 265 | ], 266 | ), 267 | _DeliveryProcess( 268 | 'In Transit', 269 | messages: [ 270 | _DeliveryMessage('13:00pm', 'Driver arrived at destination'), 271 | _DeliveryMessage('11:35am', 'Package delivered by m.vassiliades'), 272 | ], 273 | ), 274 | _DeliveryProcess.complete(), 275 | ], 276 | ); 277 | 278 | class _OrderInfo { 279 | const _OrderInfo({ 280 | required this.id, 281 | required this.date, 282 | required this.driverInfo, 283 | required this.deliveryProcesses, 284 | }); 285 | 286 | final int id; 287 | final DateTime date; 288 | final _DriverInfo driverInfo; 289 | final List<_DeliveryProcess> deliveryProcesses; 290 | } 291 | 292 | class _DriverInfo { 293 | const _DriverInfo({ 294 | required this.name, 295 | required this.thumbnailUrl, 296 | }); 297 | 298 | final String name; 299 | final String thumbnailUrl; 300 | } 301 | 302 | class _DeliveryProcess { 303 | const _DeliveryProcess( 304 | this.name, { 305 | this.messages = const [], 306 | }); 307 | 308 | const _DeliveryProcess.complete() 309 | : this.name = 'Done', 310 | this.messages = const []; 311 | 312 | final String name; 313 | final List<_DeliveryMessage> messages; 314 | 315 | bool get isCompleted => name == 'Done'; 316 | } 317 | 318 | class _DeliveryMessage { 319 | const _DeliveryMessage(this.createdAt, this.message); 320 | 321 | final String createdAt; // final DateTime createdAt; 322 | final String message; 323 | 324 | @override 325 | String toString() { 326 | return '$createdAt $message'; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /example/lib/showcase/process_timeline.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | import 'dart:math'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 6 | import 'package:timelines/timelines.dart'; 7 | 8 | import '../widget.dart'; 9 | 10 | const kTileHeight = 50.0; 11 | 12 | const completeColor = Color(0xff5e6172); 13 | const inProgressColor = Color(0xff5ec792); 14 | const todoColor = Color(0xffd1d2d7); 15 | 16 | class ProcessTimelinePage extends StatefulWidget { 17 | @override 18 | _ProcessTimelinePageState createState() => _ProcessTimelinePageState(); 19 | } 20 | 21 | class _ProcessTimelinePageState extends State { 22 | int _processIndex = 2; 23 | 24 | Color getColor(int index) { 25 | if (index == _processIndex) { 26 | return inProgressColor; 27 | } else if (index < _processIndex) { 28 | return completeColor; 29 | } else { 30 | return todoColor; 31 | } 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | backgroundColor: Colors.white, 38 | appBar: TitleAppBar('Process Timeline'), 39 | body: Timeline.tileBuilder( 40 | theme: TimelineThemeData( 41 | direction: Axis.horizontal, 42 | connectorTheme: ConnectorThemeData( 43 | space: 30.0, 44 | thickness: 5.0, 45 | ), 46 | ), 47 | builder: TimelineTileBuilder.connected( 48 | connectionDirection: ConnectionDirection.before, 49 | itemExtentBuilder: (_, __) => 50 | MediaQuery.of(context).size.width / _processes.length, 51 | oppositeContentsBuilder: (context, index) { 52 | return Padding( 53 | padding: const EdgeInsets.only(bottom: 15.0), 54 | child: Image.asset( 55 | 'assets/images/process_timeline/status${index + 1}.png', 56 | width: 50.0, 57 | color: getColor(index), 58 | ), 59 | ); 60 | }, 61 | contentsBuilder: (context, index) { 62 | return Padding( 63 | padding: const EdgeInsets.only(top: 15.0), 64 | child: Text( 65 | _processes[index], 66 | style: TextStyle( 67 | fontWeight: FontWeight.bold, 68 | color: getColor(index), 69 | ), 70 | ), 71 | ); 72 | }, 73 | indicatorBuilder: (_, index) { 74 | var color; 75 | var child; 76 | if (index == _processIndex) { 77 | color = inProgressColor; 78 | child = Padding( 79 | padding: const EdgeInsets.all(8.0), 80 | child: CircularProgressIndicator( 81 | strokeWidth: 3.0, 82 | valueColor: AlwaysStoppedAnimation(Colors.white), 83 | ), 84 | ); 85 | } else if (index < _processIndex) { 86 | color = completeColor; 87 | child = Icon( 88 | Icons.check, 89 | color: Colors.white, 90 | size: 15.0, 91 | ); 92 | } else { 93 | color = todoColor; 94 | } 95 | 96 | if (index <= _processIndex) { 97 | return Stack( 98 | children: [ 99 | CustomPaint( 100 | size: Size(30.0, 30.0), 101 | painter: _BezierPainter( 102 | color: color, 103 | drawStart: index > 0, 104 | drawEnd: index < _processIndex, 105 | ), 106 | ), 107 | DotIndicator( 108 | size: 30.0, 109 | color: color, 110 | child: child, 111 | ), 112 | ], 113 | ); 114 | } else { 115 | return Stack( 116 | children: [ 117 | CustomPaint( 118 | size: Size(15.0, 15.0), 119 | painter: _BezierPainter( 120 | color: color, 121 | drawEnd: index < _processes.length - 1, 122 | ), 123 | ), 124 | OutlinedDotIndicator( 125 | borderWidth: 4.0, 126 | color: color, 127 | ), 128 | ], 129 | ); 130 | } 131 | }, 132 | connectorBuilder: (_, index, type) { 133 | if (index > 0) { 134 | if (index == _processIndex) { 135 | final prevColor = getColor(index - 1); 136 | final color = getColor(index); 137 | List gradientColors; 138 | if (type == ConnectorType.start) { 139 | gradientColors = [Color.lerp(prevColor, color, 0.5)!, color]; 140 | } else { 141 | gradientColors = [ 142 | prevColor, 143 | Color.lerp(prevColor, color, 0.5)! 144 | ]; 145 | } 146 | return DecoratedLineConnector( 147 | decoration: BoxDecoration( 148 | gradient: LinearGradient( 149 | colors: gradientColors, 150 | ), 151 | ), 152 | ); 153 | } else { 154 | return SolidLineConnector( 155 | color: getColor(index), 156 | ); 157 | } 158 | } else { 159 | return null; 160 | } 161 | }, 162 | itemCount: _processes.length, 163 | ), 164 | ), 165 | floatingActionButton: FloatingActionButton( 166 | child: Icon(FontAwesomeIcons.chevronRight), 167 | onPressed: () { 168 | setState(() { 169 | _processIndex = (_processIndex + 1) % _processes.length; 170 | }); 171 | }, 172 | backgroundColor: inProgressColor, 173 | ), 174 | ); 175 | } 176 | } 177 | 178 | /// hardcoded bezier painter 179 | /// TODO: Bezier curve into package component 180 | class _BezierPainter extends CustomPainter { 181 | const _BezierPainter({ 182 | required this.color, 183 | this.drawStart = true, 184 | this.drawEnd = true, 185 | }); 186 | 187 | final Color color; 188 | final bool drawStart; 189 | final bool drawEnd; 190 | 191 | Offset _offset(double radius, double angle) { 192 | return Offset( 193 | radius * cos(angle) + radius, 194 | radius * sin(angle) + radius, 195 | ); 196 | } 197 | 198 | @override 199 | void paint(Canvas canvas, Size size) { 200 | final paint = Paint() 201 | ..style = PaintingStyle.fill 202 | ..color = color; 203 | 204 | final radius = size.width / 2; 205 | 206 | var angle; 207 | var offset1; 208 | var offset2; 209 | 210 | var path; 211 | 212 | if (drawStart) { 213 | angle = 3 * pi / 4; 214 | offset1 = _offset(radius, angle); 215 | offset2 = _offset(radius, -angle); 216 | path = Path() 217 | ..moveTo(offset1.dx, offset1.dy) 218 | ..quadraticBezierTo(0.0, size.height / 2, -radius, 219 | radius) // TODO connector start & gradient 220 | ..quadraticBezierTo(0.0, size.height / 2, offset2.dx, offset2.dy) 221 | ..close(); 222 | 223 | canvas.drawPath(path, paint); 224 | } 225 | if (drawEnd) { 226 | angle = -pi / 4; 227 | offset1 = _offset(radius, angle); 228 | offset2 = _offset(radius, -angle); 229 | 230 | path = Path() 231 | ..moveTo(offset1.dx, offset1.dy) 232 | ..quadraticBezierTo(size.width, size.height / 2, size.width + radius, 233 | radius) // TODO connector end & gradient 234 | ..quadraticBezierTo(size.width, size.height / 2, offset2.dx, offset2.dy) 235 | ..close(); 236 | 237 | canvas.drawPath(path, paint); 238 | } 239 | } 240 | 241 | @override 242 | bool shouldRepaint(_BezierPainter oldDelegate) { 243 | return oldDelegate.color != color || 244 | oldDelegate.drawStart != drawStart || 245 | oldDelegate.drawEnd != drawEnd; 246 | } 247 | } 248 | 249 | final _processes = [ 250 | 'Prospect', 251 | 'Tour', 252 | 'Offer', 253 | 'Contract', 254 | 'Settled', 255 | ]; 256 | -------------------------------------------------------------------------------- /example/lib/showcase/timeline_status.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:timelines/timelines.dart'; 3 | 4 | import '../widget.dart'; 5 | 6 | const kTileHeight = 50.0; 7 | 8 | class TimelineStatusPage extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: TitleAppBar('Timeline Status'), 13 | body: Center( 14 | child: Padding( 15 | padding: const EdgeInsets.symmetric(horizontal: 12.0), 16 | child: Row( 17 | children: [ 18 | _Timeline1(), 19 | SizedBox(width: 12.0), 20 | _Timeline2(), 21 | SizedBox(width: 12.0), 22 | _Timeline3(), 23 | ], 24 | ), 25 | ), 26 | ), 27 | ); 28 | } 29 | } 30 | 31 | class _Timeline1 extends StatelessWidget { 32 | @override 33 | Widget build(BuildContext context) { 34 | final data = _TimelineStatus.values; 35 | return Flexible( 36 | child: Timeline.tileBuilder( 37 | theme: TimelineThemeData( 38 | nodePosition: 0, 39 | connectorTheme: ConnectorThemeData( 40 | thickness: 3.0, 41 | color: Color(0xffd3d3d3), 42 | ), 43 | indicatorTheme: IndicatorThemeData( 44 | size: 15.0, 45 | ), 46 | ), 47 | padding: EdgeInsets.symmetric(vertical: 20.0), 48 | builder: TimelineTileBuilder.connected( 49 | contentsBuilder: (_, __) => _EmptyContents(), 50 | connectorBuilder: (_, index, __) { 51 | if (index == 0) { 52 | return SolidLineConnector(color: Color(0xff6ad192)); 53 | } else { 54 | return SolidLineConnector(); 55 | } 56 | }, 57 | indicatorBuilder: (_, index) { 58 | switch (data[index]) { 59 | case _TimelineStatus.done: 60 | return DotIndicator( 61 | color: Color(0xff6ad192), 62 | child: Icon( 63 | Icons.check, 64 | color: Colors.white, 65 | size: 10.0, 66 | ), 67 | ); 68 | case _TimelineStatus.sync: 69 | return DotIndicator( 70 | color: Color(0xff193fcc), 71 | child: Icon( 72 | Icons.sync, 73 | size: 10.0, 74 | color: Colors.white, 75 | ), 76 | ); 77 | case _TimelineStatus.inProgress: 78 | return OutlinedDotIndicator( 79 | color: Color(0xffa7842a), 80 | borderWidth: 2.0, 81 | backgroundColor: Color(0xffebcb62), 82 | ); 83 | case _TimelineStatus.todo: 84 | default: 85 | return OutlinedDotIndicator( 86 | color: Color(0xffbabdc0), 87 | backgroundColor: Color(0xffe6e7e9), 88 | ); 89 | } 90 | }, 91 | itemExtentBuilder: (_, __) => kTileHeight, 92 | itemCount: data.length, 93 | ), 94 | ), 95 | ); 96 | } 97 | } 98 | 99 | class _Timeline2 extends StatelessWidget { 100 | @override 101 | Widget build(BuildContext context) { 102 | List<_TimelineStatus> data = [ 103 | _TimelineStatus.done, 104 | _TimelineStatus.inProgress, 105 | _TimelineStatus.inProgress, 106 | _TimelineStatus.todo 107 | ]; 108 | 109 | return Flexible( 110 | child: Timeline.tileBuilder( 111 | theme: TimelineThemeData( 112 | nodePosition: 0, 113 | color: Color(0xffc2c5c9), 114 | connectorTheme: ConnectorThemeData( 115 | thickness: 3.0, 116 | ), 117 | ), 118 | padding: EdgeInsets.only(top: 20.0), 119 | builder: TimelineTileBuilder.connected( 120 | indicatorBuilder: (context, index) { 121 | return DotIndicator( 122 | color: data[index].isInProgress ? Color(0xff193fcc) : null, 123 | ); 124 | }, 125 | connectorBuilder: (_, index, connectorType) { 126 | var color; 127 | if (index + 1 < data.length - 1) { 128 | color = data[index].isInProgress && data[index + 1].isInProgress 129 | ? Color(0xff193fcc) 130 | : null; 131 | } 132 | return SolidLineConnector( 133 | indent: connectorType == ConnectorType.start ? 0 : 2.0, 134 | endIndent: connectorType == ConnectorType.end ? 0 : 2.0, 135 | color: color, 136 | ); 137 | }, 138 | contentsBuilder: (_, __) => _EmptyContents(), 139 | itemExtentBuilder: (_, __) { 140 | return kTileHeight; 141 | }, 142 | itemCount: data.length, 143 | ), 144 | ), 145 | ); 146 | } 147 | } 148 | 149 | class _Timeline3 extends StatelessWidget { 150 | @override 151 | Widget build(BuildContext context) { 152 | List<_TimelineStatus> data = [ 153 | _TimelineStatus.done, 154 | _TimelineStatus.inProgress, 155 | _TimelineStatus.inProgress, 156 | _TimelineStatus.todo 157 | ]; 158 | 159 | return Flexible( 160 | child: Timeline.tileBuilder( 161 | theme: TimelineThemeData( 162 | nodePosition: 0, 163 | nodeItemOverlap: true, 164 | connectorTheme: ConnectorThemeData( 165 | color: Color(0xffe6e7e9), 166 | thickness: 15.0, 167 | ), 168 | ), 169 | padding: EdgeInsets.only(top: 20.0), 170 | builder: TimelineTileBuilder.connected( 171 | indicatorBuilder: (context, index) { 172 | final status = data[index]; 173 | return OutlinedDotIndicator( 174 | color: 175 | status.isInProgress ? Color(0xff6ad192) : Color(0xffe6e7e9), 176 | backgroundColor: 177 | status.isInProgress ? Color(0xffd4f5d6) : Color(0xffc2c5c9), 178 | borderWidth: status.isInProgress ? 3.0 : 2.5, 179 | ); 180 | }, 181 | connectorBuilder: (context, index, connectorType) { 182 | var color; 183 | if (index + 1 < data.length - 1 && 184 | data[index].isInProgress && 185 | data[index + 1].isInProgress) { 186 | color = data[index].isInProgress ? Color(0xff6ad192) : null; 187 | } 188 | return SolidLineConnector( 189 | color: color, 190 | ); 191 | }, 192 | contentsBuilder: (context, index) { 193 | var height; 194 | if (index + 1 < data.length - 1 && 195 | data[index].isInProgress && 196 | data[index + 1].isInProgress) { 197 | height = kTileHeight - 10; 198 | } else { 199 | height = kTileHeight + 5; 200 | } 201 | return SizedBox( 202 | height: height, 203 | child: Align( 204 | alignment: Alignment.centerLeft, 205 | child: _EmptyContents(), 206 | ), 207 | ); 208 | }, 209 | itemCount: data.length, 210 | ), 211 | ), 212 | ); 213 | } 214 | } 215 | 216 | class _EmptyContents extends StatelessWidget { 217 | @override 218 | Widget build(BuildContext context) { 219 | return Container( 220 | margin: EdgeInsets.only(left: 10.0), 221 | height: 10.0, 222 | decoration: BoxDecoration( 223 | borderRadius: BorderRadius.circular(2.0), 224 | color: Color(0xffe6e7e9), 225 | ), 226 | ); 227 | } 228 | } 229 | 230 | enum _TimelineStatus { 231 | done, 232 | sync, 233 | inProgress, 234 | todo, 235 | } 236 | 237 | extension on _TimelineStatus { 238 | bool get isInProgress => this == _TimelineStatus.inProgress; 239 | } 240 | -------------------------------------------------------------------------------- /example/lib/showcase_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 4 | import 'package:url_launcher/url_launcher.dart'; 5 | 6 | import 'showcase/package_delivery_tracking.dart'; 7 | import 'showcase/process_timeline.dart'; 8 | import 'showcase/timeline_status.dart'; 9 | import 'widget.dart'; 10 | 11 | class ShowcasePage extends StatelessWidget { 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: TitleAppBar('Showcase'), 16 | body: LayoutBuilder( 17 | builder: (context, constraints) { 18 | final cards = [ 19 | _ShowcaseCard( 20 | image: 'assets/images/timeline_status.png', 21 | title: 'Timeline Status', 22 | designer: 'Tridip Thrizu', 23 | url: 24 | 'https://dribbble.com/shots/5659998-Daily-UI-Component-4-Timeline-Status', 25 | navigationBuilder: () => TimelineStatusPage(), 26 | ), 27 | _ShowcaseCard( 28 | image: 'assets/images/package_delivery_tracking.png', 29 | title: 'Package Delivery Tracking', 30 | designer: 'Series Eight', 31 | url: 32 | 'https://dribbble.com/shots/1899993-Package-Delivery-Tracking/attachments/1899993-Package-Delivery-Tracking?mode=media', 33 | navigationBuilder: () => PackageDeliveryTrackingPage(), 34 | ), 35 | _ShowcaseCard( 36 | image: 'assets/images/process_timeline.png', 37 | title: 'Process Timeline', 38 | designer: 'Eddie Lobanovskiy', 39 | url: 'https://dribbble.com/shots/5260798-Process', 40 | navigationBuilder: () => ProcessTimelinePage(), 41 | ), 42 | ]; 43 | 44 | if (constraints.maxWidth >= 760) { 45 | return SingleChildScrollView( 46 | padding: EdgeInsets.all(40.0), 47 | child: Center( 48 | child: Wrap( 49 | children: cards 50 | .map( 51 | (card) => SizedBox(width: 320.0, child: card), 52 | ) 53 | .toList(), 54 | ), 55 | ), 56 | ); 57 | } else { 58 | return ListView( 59 | padding: EdgeInsets.symmetric(vertical: 20.0), 60 | children: cards, 61 | ); 62 | } 63 | }, 64 | ), 65 | ); 66 | } 67 | } 68 | 69 | class _ShowcaseCard extends StatelessWidget { 70 | const _ShowcaseCard({ 71 | Key? key, 72 | required this.navigationBuilder, 73 | required this.image, 74 | required this.title, 75 | required this.designer, 76 | required this.url, 77 | }) : super(key: key); 78 | 79 | final String image; 80 | final String title; 81 | final String designer; 82 | final String url; 83 | 84 | final NavigateWidgetBuilder navigationBuilder; 85 | 86 | Widget _forceLightTheme(BuildContext context, Widget child) { 87 | return Theme( 88 | data: ThemeData.light(), 89 | child: child, 90 | ); 91 | } 92 | 93 | @override 94 | Widget build(BuildContext context) { 95 | return NavigationCard( 96 | navigationBuilder: () => _forceLightTheme(context, navigationBuilder()), 97 | child: Container( 98 | child: Column( 99 | mainAxisSize: MainAxisSize.min, 100 | children: [ 101 | Image.asset( 102 | image, 103 | width: MediaQuery.of(context).size.width, 104 | fit: BoxFit.fitWidth, 105 | ), 106 | Container( 107 | child: Column( 108 | children: [ 109 | SizedBox(height: 20.0), 110 | Text(title), 111 | Container( 112 | padding: EdgeInsets.all(12.0), 113 | alignment: Alignment.centerRight, 114 | child: InkWell( 115 | child: Container( 116 | padding: EdgeInsets.all(8.0), 117 | child: Row( 118 | mainAxisSize: MainAxisSize.min, 119 | children: [ 120 | Icon( 121 | FontAwesomeIcons.dribbble, 122 | semanticLabel: 'Original', 123 | size: 10.0, 124 | color: Colors.grey[600], 125 | ), 126 | SizedBox(width: 6.0), 127 | Flexible( 128 | child: Text( 129 | 'Designed by $designer', 130 | maxLines: 1, 131 | overflow: TextOverflow.ellipsis, 132 | style: TextStyle( 133 | fontSize: 10.0, 134 | color: Colors.grey[600], 135 | ), 136 | ), 137 | ), 138 | ], 139 | ), 140 | ), 141 | onTap: () async { 142 | if (await canLaunch(url)) await launch(url); 143 | }, 144 | ), 145 | ), 146 | ], 147 | ), 148 | ), 149 | ], 150 | ), 151 | ), 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /example/lib/theme_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:timelines/timelines.dart'; 4 | 5 | import 'widget.dart'; 6 | 7 | class ThemePage extends StatefulWidget { 8 | @override 9 | _ThemePageState createState() => _ThemePageState(); 10 | } 11 | 12 | class _ThemePageState extends State { 13 | final _themeColors = { 14 | 'RED': Colors.red, 15 | 'GREEN': Colors.green, 16 | 'BLUE': Colors.blue, 17 | 'AMBER': Colors.amber, 18 | 'TEAL': Colors.teal, 19 | 'ORANGE': Colors.orange, 20 | }; 21 | 22 | late TimelineThemeData _theme; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _theme = TimelineThemeData(); 28 | } 29 | 30 | void _updateTheme(TimelineThemeData theme) { 31 | if (_theme != theme) { 32 | setState(() { 33 | _theme = theme; 34 | }); 35 | } 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return Scaffold( 41 | appBar: TitleAppBar('Theme'), 42 | body: Stack( 43 | children: [ 44 | ListView( 45 | padding: EdgeInsets.fromLTRB(20.0, 160.0, 20.0, 40.0), 46 | children: [ 47 | Card( 48 | child: Container( 49 | padding: EdgeInsets.all(20.0), 50 | child: Row( 51 | children: [ 52 | Text('contents: '), 53 | Container( 54 | width: 10.0, 55 | height: 10.0, 56 | color: Colors.teal, 57 | ), 58 | SizedBox(width: 10.0), 59 | Text('opposite contents: '), 60 | Container( 61 | width: 10.0, 62 | height: 10.0, 63 | color: Colors.amber, 64 | ), 65 | ], 66 | ), 67 | ), 68 | ), 69 | SizedBox(height: 10.0), 70 | Card( 71 | child: Container( 72 | padding: EdgeInsets.all(20.0), 73 | child: Column( 74 | children: [ 75 | Text( 76 | 'TimelineTheme', 77 | style: Theme.of(context).textTheme.headline6, 78 | ), 79 | _ThemeDropdown( 80 | title: 'Direction', 81 | items: { 82 | 'Vertical': Axis.vertical, 83 | 'Horizontal': Axis.horizontal, 84 | }, 85 | value: _theme.direction, 86 | onChanged: (Axis? axis) { 87 | if (_theme.direction != axis) { 88 | setState(() { 89 | _updateTheme(_theme.copyWith(direction: axis)); 90 | }); 91 | } 92 | }, 93 | ), 94 | _ThemeDropdown( 95 | title: 'Color', 96 | items: _themeColors, 97 | value: _theme.color, 98 | onChanged: (Color? color) { 99 | _updateTheme(_theme.copyWith(color: color)); 100 | }, 101 | ), 102 | SizedBox(height: 10.0), 103 | Row( 104 | children: [ 105 | Text('Node item overlap'), 106 | SizedBox(width: 12.0), 107 | Checkbox( 108 | value: _theme.nodeItemOverlap, 109 | onChanged: (overlap) { 110 | _updateTheme( 111 | _theme.copyWith(nodeItemOverlap: overlap)); 112 | }, 113 | ), 114 | ], 115 | ), 116 | _ThemeSlider( 117 | title: 'Node Position', 118 | value: _theme.nodePosition, 119 | onChanged: (nodePosition) { 120 | _updateTheme( 121 | _theme.copyWith(nodePosition: nodePosition)); 122 | }, 123 | ), 124 | _ThemeSlider( 125 | title: 'Indicator Position', 126 | value: _theme.indicatorPosition, 127 | onChanged: (indicatorPosition) { 128 | _updateTheme(_theme.copyWith( 129 | indicatorPosition: indicatorPosition)); 130 | }, 131 | ), 132 | ], 133 | ), 134 | ), 135 | ), 136 | Card( 137 | child: Container( 138 | padding: EdgeInsets.all(20.0), 139 | child: Column( 140 | children: [ 141 | Text( 142 | 'IndicatorTheme', 143 | style: Theme.of(context).textTheme.headline6, 144 | ), 145 | _ThemeDropdown( 146 | title: 'Color', 147 | items: _themeColors, 148 | value: _theme.indicatorTheme.color, 149 | onChanged: (color) { 150 | _updateTheme( 151 | _theme.copyWith( 152 | indicatorTheme: 153 | _theme.indicatorTheme.copyWith(color: color), 154 | ), 155 | ); 156 | }, 157 | ), 158 | SizedBox(height: 10.0), 159 | _ThemeSlider( 160 | title: 'Position', 161 | value: _theme.indicatorTheme.position ?? 0, 162 | onChanged: (position) { 163 | _updateTheme( 164 | _theme.copyWith( 165 | indicatorTheme: _theme.indicatorTheme 166 | .copyWith(position: position), 167 | ), 168 | ); 169 | }, 170 | ), 171 | _ThemeSlider( 172 | title: 'Size', 173 | value: _theme.indicatorTheme.size ?? 0, 174 | max: 100.0, 175 | onChanged: (size) { 176 | _updateTheme( 177 | _theme.copyWith( 178 | indicatorTheme: 179 | _theme.indicatorTheme.copyWith(size: size), 180 | ), 181 | ); 182 | }, 183 | ), 184 | ], 185 | ), 186 | ), 187 | ), 188 | Card( 189 | child: Container( 190 | padding: EdgeInsets.all(20.0), 191 | child: Column( 192 | children: [ 193 | Text( 194 | 'ConnectorTheme', 195 | style: Theme.of(context).textTheme.headline6, 196 | ), 197 | _ThemeDropdown( 198 | title: 'Color', 199 | items: _themeColors, 200 | value: _theme.connectorTheme.color, 201 | onChanged: (color) { 202 | _updateTheme( 203 | _theme.copyWith( 204 | connectorTheme: 205 | _theme.connectorTheme.copyWith(color: color), 206 | ), 207 | ); 208 | }, 209 | ), 210 | SizedBox(height: 10.0), 211 | _ThemeSlider( 212 | title: 'Space', 213 | value: _theme.connectorTheme.space ?? 0, 214 | max: 100, 215 | onChanged: (space) { 216 | _updateTheme( 217 | _theme.copyWith( 218 | connectorTheme: 219 | _theme.connectorTheme.copyWith(space: space), 220 | ), 221 | ); 222 | }, 223 | ), 224 | _ThemeSlider( 225 | title: 'Indent', 226 | value: _theme.connectorTheme.indent ?? 0, 227 | max: 22, 228 | onChanged: (indent) { 229 | _updateTheme( 230 | _theme.copyWith( 231 | connectorTheme: _theme.connectorTheme 232 | .copyWith(indent: indent), 233 | ), 234 | ); 235 | }, 236 | ), 237 | _ThemeSlider( 238 | title: 'Thickness', 239 | value: _theme.connectorTheme.thickness ?? 0, 240 | max: 100, 241 | onChanged: (thickness) { 242 | _updateTheme( 243 | _theme.copyWith( 244 | connectorTheme: _theme.connectorTheme 245 | .copyWith(thickness: thickness), 246 | ), 247 | ); 248 | }, 249 | ), 250 | ], 251 | ), 252 | ), 253 | ), 254 | ], 255 | ), 256 | Card( 257 | elevation: 3, 258 | margin: EdgeInsets.zero, 259 | child: Container( 260 | padding: EdgeInsets.all(20.0), 261 | child: TimelineTheme( 262 | data: _theme, 263 | child: Column( 264 | mainAxisSize: MainAxisSize.min, 265 | children: [ 266 | TimelineTile( 267 | mainAxisExtent: 100, 268 | crossAxisExtent: 100, 269 | oppositeContents: Container(color: Colors.amber), 270 | node: TimelineNode( 271 | startConnector: SolidLineConnector(), 272 | endConnector: SolidLineConnector(), 273 | indicator: OutlinedDotIndicator(), 274 | ), 275 | contents: Container(color: Colors.teal), 276 | ), 277 | ], 278 | ), 279 | ), 280 | ), 281 | ), 282 | ], 283 | ), 284 | ); 285 | } 286 | } 287 | 288 | class _ThemeDropdown extends StatelessWidget { 289 | const _ThemeDropdown({ 290 | Key? key, 291 | required this.title, 292 | required this.items, 293 | required this.value, 294 | required this.onChanged, 295 | }) : super(key: key); 296 | 297 | final String title; 298 | final Map items; 299 | final T value; 300 | final ValueChanged onChanged; 301 | 302 | @override 303 | Widget build(BuildContext context) { 304 | return Row( 305 | children: [ 306 | Text(title), 307 | SizedBox(width: 10.0), 308 | DropdownButton( 309 | items: items.entries.map((entry) { 310 | return DropdownMenuItem( 311 | value: entry.value, 312 | child: Text(entry.key), 313 | ); 314 | }).toList(), 315 | value: value, 316 | onChanged: onChanged, 317 | ), 318 | ], 319 | ); 320 | } 321 | } 322 | 323 | class _ThemeSlider extends StatelessWidget { 324 | const _ThemeSlider({ 325 | Key? key, 326 | required this.title, 327 | required this.value, 328 | required this.onChanged, 329 | this.max = 1.0, 330 | }) : super(key: key); 331 | 332 | final String title; 333 | final double? value; 334 | final ValueChanged onChanged; 335 | final double max; 336 | 337 | @override 338 | Widget build(BuildContext context) { 339 | var label; 340 | if (value == null) { 341 | label = ''; 342 | } else if (value! > 1) { 343 | label = value!.toInt().toString(); 344 | } else { 345 | label = value.toString(); 346 | } 347 | return Column( 348 | crossAxisAlignment: CrossAxisAlignment.start, 349 | children: [ 350 | Text(title), 351 | SizedBox(width: 10.0), 352 | Slider( 353 | label: label, 354 | max: max, 355 | divisions: 100, 356 | value: value ?? 0, 357 | onChanged: onChanged, 358 | ), 359 | ], 360 | ); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /example/lib/widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | typedef NavigateWidgetBuilder = Widget Function(); 4 | 5 | mixin NavigateMixin on Widget { 6 | NavigateWidgetBuilder? get navigationBuilder; 7 | 8 | Future navigate(BuildContext context) { 9 | if (navigationBuilder == null) { 10 | return Future.value(); 11 | } else { 12 | return Navigator.push( 13 | context, 14 | MaterialPageRoute( 15 | builder: (context) => navigationBuilder!(), 16 | ), 17 | ); 18 | } 19 | } 20 | } 21 | 22 | const kNavigationCardRadius = 8.0; 23 | 24 | class NavigationCard extends StatelessWidget with NavigateMixin { 25 | const NavigationCard({ 26 | Key? key, 27 | this.margin, 28 | this.borderRadius = 29 | const BorderRadius.all(Radius.circular(kNavigationCardRadius)), 30 | this.navigationBuilder, 31 | required this.child, 32 | }) : super(key: key); 33 | 34 | final EdgeInsetsGeometry? margin; 35 | final BorderRadius? borderRadius; 36 | final Widget child; 37 | final NavigateWidgetBuilder? navigationBuilder; 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return Card( 42 | clipBehavior: Clip.antiAliasWithSaveLayer, 43 | margin: margin, 44 | shape: borderRadius != null 45 | ? RoundedRectangleBorder(borderRadius: borderRadius!) 46 | : null, 47 | child: InkWell( 48 | borderRadius: borderRadius, 49 | onTap: () => navigate(context), 50 | child: child, 51 | ), 52 | ); 53 | } 54 | } 55 | 56 | class TitleAppBar extends StatelessWidget with PreferredSizeWidget { 57 | TitleAppBar( 58 | this.title, { 59 | Key? key, 60 | }) : preferredSize = Size.fromHeight(kToolbarHeight), 61 | super(key: key); 62 | 63 | @override 64 | final Size preferredSize; 65 | 66 | final String title; 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | return AppBar( 71 | title: Text(title), 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter application. 3 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: '>=2.12.0 <3.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | timelines: 14 | path: ../ 15 | 16 | 17 | font_awesome_flutter: ^9.0.0 18 | url_launcher: ^6.0.3 19 | url_launcher_web: ^2.0.0 20 | 21 | dev_dependencies: 22 | flutter_test: 23 | sdk: flutter 24 | 25 | flutter: 26 | uses-material-design: true 27 | assets: 28 | - assets/images/ 29 | - assets/images/process_timeline/ 30 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | example 30 | 31 | 32 | 33 | 36 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter application.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/connector_theme.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' show lerpDouble; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'connectors.dart'; 8 | import 'timeline_node.dart'; 9 | import 'timeline_theme.dart'; 10 | 11 | /// Defines the visual properties of [SolidLineConnector], connectors inside 12 | /// [TimelineNode]. 13 | /// 14 | /// Descendant widgets obtain the current [ConnectorThemeData] object using 15 | /// `ConnectorTheme.of(context)`. Instances of [ConnectorThemeData] can be 16 | /// customized with [ConnectorThemeData.copyWith]. 17 | /// 18 | /// Typically a [ConnectorThemeData] is specified as part of the overall 19 | /// [TimelineTheme] with [TimelineThemeData.connectorTheme]. 20 | /// 21 | /// All [ConnectorThemeData] properties are `null` by default. When null, the 22 | /// widgets will provide their own defaults. 23 | /// 24 | /// See also: 25 | /// 26 | /// * [TimelineThemeData], which describes the overall theme information for 27 | /// the timeline. 28 | @immutable 29 | class ConnectorThemeData with Diagnosticable { 30 | /// Creates a theme that can be used for [ConnectorTheme] or 31 | /// [TimelineThemeData.connectorTheme]. 32 | const ConnectorThemeData({ 33 | this.color, 34 | this.space, 35 | this.thickness, 36 | this.indent, 37 | }); 38 | 39 | /// The color of [SolidLineConnector]s and connectors inside [TimelineNode]s, 40 | /// and so forth. 41 | final Color? color; 42 | 43 | /// This represents the amount of horizontal or vertical space the connector 44 | /// takes up. 45 | final double? space; 46 | 47 | /// The thickness of the line drawn within the connector. 48 | final double? thickness; 49 | 50 | /// The amount of empty space at the edge of [SolidLineConnector]. 51 | final double? indent; 52 | 53 | /// Creates a copy of this object with the given fields replaced with the new 54 | /// values. 55 | ConnectorThemeData copyWith({ 56 | Color? color, 57 | double? space, 58 | double? thickness, 59 | double? indent, 60 | }) { 61 | return ConnectorThemeData( 62 | color: color ?? this.color, 63 | space: space ?? this.space, 64 | thickness: thickness ?? this.thickness, 65 | indent: indent ?? this.indent, 66 | ); 67 | } 68 | 69 | /// Linearly interpolate between two Connector themes. 70 | /// 71 | /// The argument `t` must not be null. 72 | /// 73 | /// {@macro dart.ui.shadow.lerp} 74 | static ConnectorThemeData lerp( 75 | ConnectorThemeData? a, ConnectorThemeData? b, double t) { 76 | return ConnectorThemeData( 77 | color: Color.lerp(a?.color, b?.color, t), 78 | space: lerpDouble(a?.space, b?.space, t), 79 | thickness: lerpDouble(a?.thickness, b?.thickness, t), 80 | indent: lerpDouble(a?.indent, b?.indent, t), 81 | ); 82 | } 83 | 84 | @override 85 | int get hashCode { 86 | return hashValues( 87 | color, 88 | space, 89 | thickness, 90 | indent, 91 | ); 92 | } 93 | 94 | @override 95 | bool operator ==(Object other) { 96 | if (identical(this, other)) return true; 97 | if (other.runtimeType != runtimeType) return false; 98 | return other is ConnectorThemeData && 99 | other.color == color && 100 | other.space == space && 101 | other.thickness == thickness && 102 | other.indent == indent; 103 | } 104 | 105 | @override 106 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 107 | super.debugFillProperties(properties); 108 | properties 109 | ..add(ColorProperty('color', color, defaultValue: null)) 110 | ..add(DoubleProperty('space', space, defaultValue: null)) 111 | ..add(DoubleProperty('thickness', thickness, defaultValue: null)) 112 | ..add(DoubleProperty('indent', indent, defaultValue: null)); 113 | } 114 | } 115 | 116 | /// An inherited widget that defines the configuration for 117 | /// [SolidLineConnector]s, connectors inside [TimelineNode]s. 118 | class ConnectorTheme extends InheritedTheme { 119 | /// Creates a connector theme that controls the configurations for 120 | /// [SolidLineConnector]s, connectors inside [TimelineNode]s. 121 | const ConnectorTheme({ 122 | Key? key, 123 | required this.data, 124 | required Widget child, 125 | }) : super(key: key, child: child); 126 | 127 | /// The properties for descendant [SolidLineConnector]s, connectors inside 128 | /// [TimelineNode]s. 129 | final ConnectorThemeData data; 130 | 131 | /// The closest instance of this class's [data] value that encloses the given 132 | /// context. 133 | /// 134 | /// If there is no ancestor, it returns [TimelineThemeData.connectorTheme]. 135 | /// Applications can assume that the returned value will not be null. 136 | /// 137 | /// Typical usage is as follows: 138 | /// 139 | /// ```dart 140 | /// ConnectorThemeData theme = ConnectorTheme.of(context); 141 | /// ``` 142 | static ConnectorThemeData of(BuildContext context) { 143 | final connectorTheme = 144 | context.dependOnInheritedWidgetOfExactType(); 145 | return connectorTheme?.data ?? TimelineTheme.of(context).connectorTheme; 146 | } 147 | 148 | @override 149 | Widget wrap(BuildContext context, Widget child) { 150 | final ancestorTheme = 151 | context.findAncestorWidgetOfExactType(); 152 | return identical(this, ancestorTheme) 153 | ? child 154 | : ConnectorTheme(data: data, child: child); 155 | } 156 | 157 | @override 158 | bool updateShouldNotify(ConnectorTheme oldWidget) => data != oldWidget.data; 159 | 160 | @override 161 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 162 | super.debugFillProperties(properties); 163 | data.debugFillProperties(properties); 164 | } 165 | } 166 | 167 | /// Connector component configured through [ConnectorTheme] 168 | mixin ThemedConnectorComponent on Widget { 169 | /// {@template timelines.connector.direction} 170 | /// If this is null, then the [TimelineThemeData.direction] is used. 171 | /// {@endtemplate} 172 | Axis? get direction; 173 | Axis getEffectiveDirection(BuildContext context) { 174 | return direction ?? TimelineTheme.of(context).direction; 175 | } 176 | 177 | /// {@template timelines.connector.thickness} 178 | /// If this is null, then the [ConnectorThemeData.thickness] is used which 179 | /// defaults to 2.0. 180 | /// {@endtemplate} 181 | double? get thickness; 182 | double getEffectiveThickness(BuildContext context) { 183 | return thickness ?? ConnectorTheme.of(context).thickness ?? 2.0; 184 | } 185 | 186 | /// {@template timelines.connector.space} 187 | /// If this is null, then the [ConnectorThemeData.space] is used. If that is 188 | /// also null, then this defaults to double.infinity. 189 | /// {@endtemplate} 190 | double? get space; 191 | double? getEffectiveSpace(BuildContext context) { 192 | return space ?? ConnectorTheme.of(context).space; 193 | } 194 | 195 | double? get indent; 196 | double getEffectiveIndent(BuildContext context) { 197 | return indent ?? ConnectorTheme.of(context).indent ?? 0.0; 198 | } 199 | 200 | double? get endIndent; 201 | double getEffectiveEndIndent(BuildContext context) { 202 | return endIndent ?? ConnectorTheme.of(context).indent ?? 0.0; 203 | } 204 | 205 | Color? get color; 206 | Color getEffectiveColor(BuildContext context) { 207 | return color ?? 208 | ConnectorTheme.of(context).color ?? 209 | TimelineTheme.of(context).color; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /lib/src/connectors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | import 'connector_theme.dart'; 5 | import 'line_painter.dart'; 6 | import 'timelines.dart'; 7 | import 'timeline_node.dart'; 8 | import 'timeline_theme.dart'; 9 | 10 | /// Abstract class for predefined connector widgets. 11 | /// 12 | /// See also: 13 | /// 14 | /// * [SolidLineConnector], which is a [Connector] that draws solid line. 15 | /// * [DashedLineConnector], which is a [Connector] that draws outlined dot. 16 | /// * [TransparentConnector], which is a [Connector] that only takes up space. 17 | abstract class Connector extends StatelessWidget with ThemedConnectorComponent { 18 | /// Creates an connector. 19 | const Connector({ 20 | Key? key, 21 | this.direction, 22 | this.space, 23 | this.thickness, 24 | this.indent, 25 | this.endIndent, 26 | this.color, 27 | }) : assert(thickness == null || thickness >= 0.0), 28 | assert(space == null || space >= 0.0), 29 | assert(indent == null || indent >= 0.0), 30 | assert(endIndent == null || endIndent >= 0.0), 31 | super(key: key); 32 | 33 | /// Creates a solid line connector. 34 | /// 35 | /// See also: 36 | /// 37 | /// * [SolidLineConnector], exactly the same. 38 | factory Connector.solidLine({ 39 | Key? key, 40 | Axis? direction, 41 | double? thickness, 42 | double? space, 43 | double? indent, 44 | double? endIndent, 45 | Color? color, 46 | }) { 47 | return SolidLineConnector( 48 | key: key, 49 | direction: direction, 50 | thickness: thickness, 51 | space: space, 52 | indent: indent, 53 | endIndent: endIndent, 54 | color: color, 55 | ); 56 | } 57 | 58 | /// Creates a dashed line connector. 59 | /// 60 | /// See also: 61 | /// 62 | /// * [DashedLineConnector], exactly the same. 63 | factory Connector.dashedLine({ 64 | Key? key, 65 | Axis? direction, 66 | double? thickness, 67 | double? dash, 68 | double? gap, 69 | double? space, 70 | double? indent, 71 | double? endIndent, 72 | Color? color, 73 | Color? gapColor, 74 | }) { 75 | return DashedLineConnector( 76 | key: key, 77 | direction: direction, 78 | thickness: thickness, 79 | dash: dash, 80 | gap: gap, 81 | space: space, 82 | indent: indent, 83 | endIndent: endIndent, 84 | color: color, 85 | gapColor: gapColor, 86 | ); 87 | } 88 | 89 | /// Creates a dashed transparent connector. 90 | /// 91 | /// See also: 92 | /// 93 | /// * [TransparentConnector], exactly the same. 94 | factory Connector.transparent({ 95 | Key? key, 96 | Axis? direction, 97 | double? indent, 98 | double? endIndent, 99 | double? space, 100 | }) { 101 | return TransparentConnector( 102 | key: key, 103 | direction: direction, 104 | indent: indent, 105 | endIndent: endIndent, 106 | space: space, 107 | ); 108 | } 109 | 110 | /// {@macro timelines.direction} 111 | /// 112 | /// {@macro timelines.connector.direction} 113 | @override 114 | final Axis? direction; 115 | 116 | /// The connector's cross axis size extent. 117 | /// 118 | /// The connector itself is always drawn as a line that is centered within the 119 | /// size specified by this value. 120 | /// {@macro timelines.connector.space} 121 | @override 122 | final double? space; 123 | 124 | /// The thickness of the line drawn within the connector. 125 | /// 126 | /// {@macro timelines.connector.thickness} 127 | @override 128 | final double? thickness; 129 | 130 | /// The amount of empty space to the leading edge of the connector. 131 | /// 132 | /// If this is null, then the [ConnectorThemeData.indent] is used. If that is 133 | /// also null, then this defaults to 0.0. 134 | @override 135 | final double? indent; 136 | 137 | /// The amount of empty space to the trailing edge of the connector. 138 | /// 139 | /// If this is null, then the [ConnectorThemeData.indent] is used. If that is 140 | /// also null, then this defaults to 0.0. 141 | @override 142 | final double? endIndent; 143 | 144 | /// The color to use when painting the line. 145 | /// 146 | /// If this is null, then the [ConnectorThemeData.color] is used. If that is 147 | /// also null, then [TimelineThemeData.color] is used. 148 | @override 149 | final Color? color; 150 | } 151 | 152 | /// A thin line, with padding on either side. 153 | /// 154 | /// The box's total cross axis size(width or height, depend on [direction]) is 155 | /// controlled by [space]. 156 | /// 157 | /// The appropriate padding is automatically computed from the cross axis size. 158 | class SolidLineConnector extends Connector { 159 | /// Creates a solid line connector. 160 | /// 161 | /// The [thickness], [space], [indent], and [endIndent] must be null or 162 | /// non-negative. 163 | const SolidLineConnector({ 164 | Key? key, 165 | Axis? direction, 166 | double? thickness, 167 | double? space, 168 | double? indent, 169 | double? endIndent, 170 | Color? color, 171 | }) : super( 172 | key: key, 173 | thickness: thickness, 174 | space: space, 175 | indent: indent, 176 | endIndent: endIndent, 177 | color: color, 178 | ); 179 | 180 | @override 181 | Widget build(BuildContext context) { 182 | final direction = getEffectiveDirection(context); 183 | final thickness = getEffectiveThickness(context); 184 | final color = getEffectiveColor(context); 185 | final space = getEffectiveSpace(context); 186 | final indent = getEffectiveIndent(context); 187 | final endIndent = getEffectiveEndIndent(context); 188 | 189 | switch (direction) { 190 | case Axis.vertical: 191 | return _ConnectorIndent( 192 | direction: direction, 193 | indent: indent, 194 | endIndent: endIndent, 195 | space: space, 196 | child: Container( 197 | width: thickness, 198 | color: color, 199 | ), 200 | ); 201 | case Axis.horizontal: 202 | return _ConnectorIndent( 203 | direction: direction, 204 | indent: indent, 205 | endIndent: endIndent, 206 | space: space, 207 | child: Container( 208 | height: thickness, 209 | color: color, 210 | ), 211 | ); 212 | } 213 | } 214 | } 215 | 216 | /// A decorated thin line, with padding on either side. 217 | /// 218 | /// The box's total cross axis size(width or height, depend on [direction]) is 219 | /// controlled by [space]. 220 | /// 221 | /// The appropriate padding is automatically computed from the cross axis size. 222 | class DecoratedLineConnector extends Connector { 223 | /// Creates a decorated line connector. 224 | /// 225 | /// The [thickness], [space], [indent], and [endIndent] must be null or 226 | /// non-negative. 227 | const DecoratedLineConnector({ 228 | Key? key, 229 | Axis? direction, 230 | double? thickness, 231 | double? space, 232 | double? indent, 233 | double? endIndent, 234 | this.decoration, 235 | }) : super( 236 | key: key, 237 | thickness: thickness, 238 | space: space, 239 | indent: indent, 240 | endIndent: endIndent, 241 | ); 242 | 243 | /// The decoration to paint line. 244 | /// 245 | /// Use the [SolidLineConnector] class to specify a simple solid color line. 246 | final Decoration? decoration; 247 | 248 | @override 249 | Widget build(BuildContext context) { 250 | final direction = getEffectiveDirection(context); 251 | final thickness = getEffectiveThickness(context); 252 | final space = getEffectiveSpace(context); 253 | final indent = getEffectiveIndent(context); 254 | final endIndent = getEffectiveEndIndent(context); 255 | final color = decoration == null ? getEffectiveColor(context) : null; 256 | 257 | switch (direction) { 258 | case Axis.vertical: 259 | return _ConnectorIndent( 260 | direction: direction, 261 | indent: indent, 262 | endIndent: endIndent, 263 | space: space, 264 | child: Container( 265 | width: thickness, 266 | color: color, 267 | decoration: decoration, 268 | ), 269 | ); 270 | case Axis.horizontal: 271 | return _ConnectorIndent( 272 | direction: direction, 273 | indent: indent, 274 | endIndent: endIndent, 275 | space: space, 276 | child: Container( 277 | height: thickness, 278 | color: color, 279 | decoration: decoration, 280 | ), 281 | ); 282 | } 283 | } 284 | } 285 | 286 | /// A thin dashed line, with padding on either side. 287 | /// 288 | /// The box's total cross axis size(width or height, depend on [direction]) is 289 | /// controlled by [space]. 290 | /// 291 | /// The appropriate padding is automatically computed from the cross axis size. 292 | /// 293 | /// See also: 294 | /// 295 | /// * [DashedLinePainter], which is painter that draws this connector. 296 | class DashedLineConnector extends Connector { 297 | /// Creates a dashed line connector. 298 | /// 299 | /// The [thickness], [space], [indent], and [endIndent] must be null or 300 | /// non-negative. 301 | const DashedLineConnector({ 302 | Key? key, 303 | Axis? direction, 304 | double? thickness, 305 | this.dash, 306 | this.gap, 307 | double? space, 308 | double? indent, 309 | double? endIndent, 310 | Color? color, 311 | this.gapColor, 312 | }) : super( 313 | key: key, 314 | direction: direction, 315 | thickness: thickness, 316 | space: space, 317 | indent: indent, 318 | endIndent: endIndent, 319 | color: color, 320 | ); 321 | 322 | /// The dash size of the line drawn within the connector. 323 | /// 324 | /// If this is null, then this defaults to 1.0. 325 | final double? dash; 326 | 327 | /// The gap of the line drawn within the connector. 328 | /// 329 | /// If this is null, then this defaults to 1.0. 330 | final double? gap; 331 | 332 | /// The color to use when painting the gap in the line. 333 | /// 334 | /// If this is null, then the [Colors.transparent] is used. 335 | final Color? gapColor; 336 | 337 | @override 338 | Widget build(BuildContext context) { 339 | final direction = getEffectiveDirection(context); 340 | return _ConnectorIndent( 341 | direction: direction, 342 | indent: getEffectiveIndent(context), 343 | endIndent: getEffectiveEndIndent(context), 344 | space: getEffectiveSpace(context), 345 | child: CustomPaint( 346 | painter: DashedLinePainter( 347 | direction: direction, 348 | color: getEffectiveColor(context), 349 | strokeWidth: getEffectiveThickness(context), 350 | dashSize: dash ?? 1.0, 351 | gapSize: gap ?? 1.0, 352 | gapColor: gapColor ?? Colors.transparent, 353 | ), 354 | child: Container(), 355 | ), 356 | ); 357 | } 358 | } 359 | 360 | /// A transparent connector for start, end [TimelineNode] of the [Timeline]. 361 | /// 362 | /// This connector will be not displayed, it only occupies an area. 363 | class TransparentConnector extends Connector { 364 | /// Creates a transparent connector. 365 | /// 366 | /// The [space], [indent], and [endIndent] must be null or non-negative. 367 | const TransparentConnector({ 368 | Key? key, 369 | Axis? direction, 370 | double? indent, 371 | double? endIndent, 372 | double? space, 373 | }) : super( 374 | key: key, 375 | direction: direction, 376 | indent: indent, 377 | endIndent: endIndent, 378 | space: space, 379 | ); 380 | 381 | @override 382 | Widget build(BuildContext context) { 383 | return _ConnectorIndent( 384 | direction: getEffectiveDirection(context), 385 | indent: getEffectiveIndent(context), 386 | endIndent: getEffectiveEndIndent(context), 387 | space: getEffectiveSpace(context), 388 | child: Container(), 389 | ); 390 | } 391 | } 392 | 393 | /// Apply indent to [child]. 394 | class _ConnectorIndent extends StatelessWidget { 395 | /// Creates a indent. 396 | /// 397 | /// The [direction]and [child] must be null. And [space], [indent] and 398 | /// [endIndent] must be null or non-negative. 399 | const _ConnectorIndent({ 400 | Key? key, 401 | required this.direction, 402 | required this.space, 403 | this.indent, 404 | this.endIndent, 405 | required this.child, 406 | }) : assert(space == null || space >= 0), 407 | assert(indent == null || indent >= 0), 408 | assert(endIndent == null || endIndent >= 0), 409 | super(key: key); 410 | 411 | /// {@macro timelines.direction} 412 | final Axis direction; 413 | 414 | /// The connector's cross axis size extent. 415 | /// 416 | /// The connector itself is always drawn as a line that is centered within the 417 | /// size specified by this value. 418 | final double? space; 419 | 420 | /// The amount of empty space to the leading edge of the connector. 421 | final double? indent; 422 | 423 | /// The amount of empty space to the trailing edge of the connector. 424 | final double? endIndent; 425 | 426 | /// The widget below this widget in the tree. 427 | /// 428 | /// {@macro flutter.widgets.child} 429 | final Widget child; 430 | 431 | @override 432 | Widget build(BuildContext context) { 433 | return SizedBox( 434 | width: direction == Axis.vertical ? space : null, 435 | height: direction == Axis.vertical ? null : space, 436 | child: Center( 437 | child: Padding( 438 | padding: direction == Axis.vertical 439 | ? EdgeInsetsDirectional.only( 440 | top: indent ?? 0, 441 | bottom: endIndent ?? 0, 442 | ) 443 | : EdgeInsetsDirectional.only( 444 | start: indent ?? 0, 445 | end: endIndent ?? 0, 446 | ), 447 | child: child, 448 | ), 449 | ), 450 | ); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /lib/src/indicator_theme.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' show lerpDouble; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'indicators.dart'; 8 | import 'timeline_node.dart'; 9 | import 'timeline_theme.dart'; 10 | 11 | /// Defines the visual properties of [DotIndicator], indicators inside 12 | /// [TimelineNode]s. 13 | /// 14 | /// Descendant widgets obtain the current [IndicatorThemeData] object using 15 | /// `IndicatorTheme.of(context)`. Instances of [IndicatorThemeData] can be 16 | /// customized with [IndicatorThemeData.copyWith]. 17 | /// 18 | /// Typically a [IndicatorThemeData] is specified as part of the overall 19 | /// [TimelineTheme] with [TimelineThemeData.indicatorTheme]. 20 | /// 21 | /// All [IndicatorThemeData] properties are `null` by default. When null, the 22 | /// widgets will provide their own defaults. 23 | /// 24 | /// See also: 25 | /// 26 | /// * [TimelineThemeData], which describes the overall theme information for 27 | /// the timeline. 28 | @immutable 29 | class IndicatorThemeData with Diagnosticable { 30 | /// Creates a theme that can be used for [IndicatorTheme] or 31 | /// [TimelineThemeData.indicatorTheme]. 32 | const IndicatorThemeData({ 33 | this.color, 34 | this.size, 35 | this.position, 36 | }); 37 | 38 | /// The color of [DotIndicator]s and indicators inside [TimelineNode]s, and so 39 | /// forth. 40 | final Color? color; 41 | 42 | /// The size of [DotIndicator]s and indicators inside [TimelineNode]s, and so 43 | /// forth in logical pixels. 44 | /// 45 | /// Indicators occupy a square with width and height equal to size. 46 | final double? size; 47 | 48 | /// A position of indicator inside both two connectors. 49 | final double? position; 50 | 51 | /// Creates a copy of this object with the given fields replaced with the new 52 | /// values. 53 | IndicatorThemeData copyWith({ 54 | Color? color, 55 | double? size, 56 | double? position, 57 | }) { 58 | return IndicatorThemeData( 59 | color: color ?? this.color, 60 | size: size ?? this.size, 61 | position: position ?? this.position, 62 | ); 63 | } 64 | 65 | /// Linearly interpolate between two Indicator themes. 66 | /// 67 | /// The argument `t` must not be null. 68 | /// 69 | /// {@macro dart.ui.shadow.lerp} 70 | static IndicatorThemeData lerp( 71 | IndicatorThemeData? a, IndicatorThemeData? b, double t) { 72 | return IndicatorThemeData( 73 | color: Color.lerp(a?.color, b?.color, t), 74 | size: lerpDouble(a?.size, b?.size, t), 75 | position: lerpDouble(a?.position, b?.position, t), 76 | ); 77 | } 78 | 79 | @override 80 | int get hashCode => hashValues(color, size, position); 81 | 82 | @override 83 | bool operator ==(Object other) { 84 | if (identical(this, other)) return true; 85 | if (other.runtimeType != runtimeType) return false; 86 | return other is IndicatorThemeData && 87 | other.color == color && 88 | other.size == size && 89 | other.position == position; 90 | } 91 | 92 | @override 93 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 94 | super.debugFillProperties(properties); 95 | properties 96 | ..add(ColorProperty('color', color, defaultValue: null)) 97 | ..add(DoubleProperty('size', size, defaultValue: null)) 98 | ..add(DoubleProperty('position', size, defaultValue: null)); 99 | } 100 | } 101 | 102 | /// Controls the default color and size of indicators in a widget subtree. 103 | /// 104 | /// The indicator theme is honored by [TimelineNode], [DotIndicator] and 105 | /// [OutlinedDotIndicator] widgets. 106 | class IndicatorTheme extends InheritedTheme { 107 | /// Creates an indicator theme that controls the color and size for 108 | /// [DotIndicator]s, indicators inside [TimelineNode]s. 109 | const IndicatorTheme({ 110 | Key? key, 111 | required this.data, 112 | required Widget child, 113 | }) : super(key: key, child: child); 114 | 115 | /// The properties for descendant [DotIndicator]s, indicators inside 116 | /// [TimelineNode]s. 117 | final IndicatorThemeData data; 118 | 119 | /// The data from the closest instance of this class that encloses the given 120 | /// context. 121 | /// 122 | /// Defaults to the current [TimelineThemeData.indicatorTheme]. 123 | /// 124 | /// Typical usage is as follows: 125 | /// 126 | /// ```dart 127 | /// IndicatorThemeData theme = IndicatorTheme.of(context); 128 | /// ``` 129 | static IndicatorThemeData of(BuildContext context) { 130 | final indicatorTheme = 131 | context.dependOnInheritedWidgetOfExactType(); 132 | return indicatorTheme?.data ?? TimelineTheme.of(context).indicatorTheme; 133 | } 134 | 135 | @override 136 | Widget wrap(BuildContext context, Widget child) { 137 | final ancestorTheme = 138 | context.findAncestorWidgetOfExactType(); 139 | return identical(this, ancestorTheme) 140 | ? child 141 | : IndicatorTheme(data: data, child: child); 142 | } 143 | 144 | @override 145 | bool updateShouldNotify(IndicatorTheme oldWidget) => data != oldWidget.data; 146 | 147 | @override 148 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 149 | super.debugFillProperties(properties); 150 | data.debugFillProperties(properties); 151 | } 152 | } 153 | 154 | /// Indicator component configured through [IndicatorTheme] 155 | mixin ThemedIndicatorComponent on PositionedIndicator { 156 | /// {@template timelines.indicator.color} 157 | /// Defaults to the current [IndicatorTheme] color, if any. 158 | /// 159 | /// If no [IndicatorTheme] and no [TimelineTheme] is specified, indicators 160 | /// will default to blue. 161 | /// {@endtemplate} 162 | Color? get color; 163 | Color getEffectiveColor(BuildContext context) { 164 | return color ?? 165 | IndicatorTheme.of(context).color ?? 166 | TimelineTheme.of(context).color; 167 | } 168 | 169 | /// {@template timelines.indicator.size} 170 | /// Indicators occupy a square with width and height equal to size. 171 | /// 172 | /// Defaults to the current [IndicatorTheme] size, if any. If there is no 173 | /// [IndicatorTheme], or it does not specify an explicit size, then it 174 | /// defaults to own child size(0.0). 175 | /// {@endtemplate} 176 | double? get size; 177 | double? getEffectiveSize(BuildContext context) { 178 | return size ?? IndicatorTheme.of(context).size; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/src/indicators.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'indicator_theme.dart'; 4 | import 'timeline_theme.dart'; 5 | 6 | /// [TimelineNode]'s indicator. 7 | mixin PositionedIndicator on Widget { 8 | /// {@template timelines.indicator.position} 9 | /// If this is null, then the [IndicatorThemeData.position] is used. If that 10 | /// is also null, then this defaults to [TimelineThemeData.indicatorPosition]. 11 | /// {@endtemplate} 12 | double? get position; 13 | double getEffectivePosition(BuildContext context) { 14 | return position ?? 15 | IndicatorTheme.of(context).position ?? 16 | TimelineTheme.of(context).indicatorPosition; 17 | } 18 | } 19 | 20 | /// Abstract class for predefined indicator widgets. 21 | /// 22 | /// See also: 23 | /// 24 | /// * [DotIndicator], which is a [Indicator] that draws dot. 25 | /// * [OutlinedDotIndicator], which is a [Indicator] that draws outlined dot. 26 | /// * [ContainerIndicator], which is a [Indicator] that draws it's child. 27 | abstract class Indicator extends StatelessWidget 28 | with PositionedIndicator, ThemedIndicatorComponent { 29 | /// Creates an indicator. 30 | const Indicator({ 31 | Key? key, 32 | this.size, 33 | this.color, 34 | this.border, 35 | this.position, 36 | this.child, 37 | }) : assert(size == null || size >= 0), 38 | assert(position == null || 0 <= position && position <= 1), 39 | super(key: key); 40 | 41 | /// Creates a dot indicator. 42 | /// 43 | /// See also: 44 | /// 45 | /// * [DotIndicator], exactly the same. 46 | factory Indicator.dot({ 47 | Key? key, 48 | double? size, 49 | Color? color, 50 | double? position, 51 | Border? border, 52 | Widget? child, 53 | }) => 54 | DotIndicator( 55 | size: size, 56 | color: color, 57 | position: position, 58 | border: border, 59 | child: child, 60 | ); 61 | 62 | /// Creates a outlined dot indicator. 63 | /// 64 | /// See also: 65 | /// 66 | /// * [OutlinedDotIndicator], exactly the same. 67 | factory Indicator.outlined({ 68 | Key? key, 69 | double? size, 70 | Color? color, 71 | Color? backgroundColor, 72 | double? position, 73 | double borderWidth = 2.0, 74 | Widget? child, 75 | }) => 76 | OutlinedDotIndicator( 77 | size: size, 78 | color: color, 79 | position: position, 80 | backgroundColor: backgroundColor, 81 | borderWidth: borderWidth, 82 | child: child, 83 | ); 84 | 85 | /// Creates a transparent indicator. 86 | /// 87 | /// See also: 88 | /// 89 | /// * [ContainerIndicator], this is created without child. 90 | factory Indicator.transparent({ 91 | Key? key, 92 | double? size, 93 | double? position, 94 | }) => 95 | ContainerIndicator( 96 | size: size, 97 | position: position, 98 | ); 99 | 100 | /// Creates a widget indicator. 101 | /// 102 | /// See also: 103 | /// 104 | /// * [OutlinedDotIndicator], exactly the same. 105 | factory Indicator.widget({ 106 | Key? key, 107 | double? size, 108 | double? position, 109 | Widget? child, 110 | }) => 111 | ContainerIndicator( 112 | size: size, 113 | position: position, 114 | child: child, 115 | ); 116 | 117 | /// The size of the dot in logical pixels. 118 | /// 119 | /// {@macro timelines.indicator.size} 120 | @override 121 | final double? size; 122 | 123 | /// The color to use when drawing the dot. 124 | /// 125 | /// {@macro timelines.indicator.color} 126 | @override 127 | final Color? color; 128 | 129 | /// The position of a indicator between the two connectors. 130 | /// 131 | /// {@macro timelines.indicator.position} 132 | @override 133 | final double? position; 134 | 135 | /// The border to use when drawing the dot's outline. 136 | final BoxBorder? border; 137 | 138 | /// The widget below this widget in the tree. 139 | /// 140 | /// {@macro flutter.widgets.child} 141 | final Widget? child; 142 | } 143 | 144 | /// A widget that displays an [child]. The [child] if null, the indicator is not 145 | /// visible. 146 | class ContainerIndicator extends Indicator { 147 | /// Creates a container indicator. 148 | const ContainerIndicator({ 149 | Key? key, 150 | double? size, 151 | double? position, 152 | this.child, 153 | }) : super( 154 | key: key, 155 | size: size, 156 | position: position, 157 | color: Colors.transparent, 158 | ); 159 | 160 | /// The widget below this widget in the tree. 161 | /// 162 | /// {@macro flutter.widgets.child} 163 | final Widget? child; 164 | 165 | @override 166 | Widget build(BuildContext context) { 167 | final size = getEffectiveSize(context); 168 | return Container( 169 | width: size, 170 | height: size, 171 | child: child, 172 | ); 173 | } 174 | } 175 | 176 | /// A widget that displays an dot. 177 | class DotIndicator extends Indicator { 178 | /// Creates a dot indicator. 179 | /// 180 | /// The [size] must be null or non-negative. 181 | const DotIndicator({ 182 | Key? key, 183 | double? size, 184 | Color? color, 185 | double? position, 186 | this.border, 187 | this.child, 188 | }) : super( 189 | key: key, 190 | size: size, 191 | color: color, 192 | position: position, 193 | ); 194 | 195 | /// The border to use when drawing the dot's outline. 196 | final BoxBorder? border; 197 | 198 | /// The widget below this widget in the tree. 199 | /// 200 | /// {@macro flutter.widgets.child} 201 | final Widget? child; 202 | 203 | @override 204 | Widget build(BuildContext context) { 205 | final effectiveSize = getEffectiveSize(context); 206 | final effectiveColor = getEffectiveColor(context); 207 | return Center( 208 | child: Container( 209 | width: effectiveSize ?? ((child == null) ? 15.0 : null), 210 | height: effectiveSize ?? ((child == null) ? 15.0 : null), 211 | child: child, 212 | decoration: BoxDecoration( 213 | shape: BoxShape.circle, 214 | color: effectiveColor, 215 | border: border, 216 | ), 217 | ), 218 | ); 219 | } 220 | } 221 | 222 | /// A widget that displays an outlined dot. 223 | class OutlinedDotIndicator extends Indicator { 224 | /// Creates a outlined dot indicator. 225 | /// 226 | /// The [size] must be null or non-negative. 227 | const OutlinedDotIndicator({ 228 | Key? key, 229 | double? size, 230 | Color? color, 231 | double? position, 232 | this.backgroundColor, 233 | this.borderWidth = 2.0, 234 | this.child, 235 | }) : assert(size == null || size >= 0), 236 | assert(position == null || 0 <= position && position <= 1), 237 | super( 238 | key: key, 239 | size: size, 240 | color: color, 241 | position: position, 242 | ); 243 | 244 | /// The color to use when drawing the dot in outline. 245 | /// 246 | /// {@macro timelines.indicator.color} 247 | final Color? backgroundColor; 248 | 249 | /// The width of this outline, in logical pixels. 250 | final double borderWidth; 251 | 252 | /// The widget below this widget in the tree. 253 | /// 254 | /// {@macro flutter.widgets.child} 255 | final Widget? child; 256 | 257 | @override 258 | Widget build(BuildContext context) { 259 | return DotIndicator( 260 | size: size, 261 | color: backgroundColor ?? Colors.transparent, 262 | position: position, 263 | border: Border.all( 264 | color: color ?? getEffectiveColor(context), 265 | width: borderWidth, 266 | ), 267 | child: child, 268 | ); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /lib/src/line_painter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | import 'connectors.dart'; 7 | 8 | /// Paints a [DashedLineConnector]. 9 | /// 10 | /// Draw the line like this: 11 | /// ``` 12 | /// 0 > [dash][gap][dash][gap] < constraints size 13 | /// ``` 14 | /// 15 | /// [dashSize] specifies the size of [dash]. and [gapSize] specifies the size of 16 | /// [gap]. 17 | /// 18 | /// When using the default colors, this painter draws a dotted line or dashed 19 | /// line that familiar. 20 | /// If set other [gapColor], this painter draws a line that alternates between 21 | /// two colors. 22 | class DashedLinePainter extends CustomPainter { 23 | /// Creates a dashed line painter. 24 | /// 25 | /// The [dashSize] argument must be 1 or more, and the [gapSize] and 26 | /// [strokeWidth] arguments must be positive numbers. 27 | /// 28 | /// The [direction], [color], [gapColor] and [strokeCap] arguments must not be 29 | /// null. 30 | const DashedLinePainter({ 31 | required this.direction, 32 | required this.color, 33 | this.gapColor = Colors.transparent, 34 | this.dashSize = 1.0, 35 | this.gapSize = 1.0, 36 | this.strokeWidth = 1.0, 37 | this.strokeCap = StrokeCap.square, 38 | }) : assert(dashSize >= 1), 39 | assert(gapSize >= 0), 40 | assert(strokeWidth >= 0); 41 | 42 | /// {@macro timelines.direction} 43 | final Axis direction; 44 | 45 | /// The color to paint dash of line. 46 | final Color color; 47 | 48 | /// The color to paint gap(another dash) of line. 49 | final Color gapColor; 50 | 51 | /// The size of dash 52 | final double dashSize; 53 | 54 | /// The size of gap, it also draws [gapColor] 55 | final double gapSize; 56 | 57 | /// The stroke width of dash and gap. 58 | final double strokeWidth; 59 | 60 | /// Styles to use for line endings. 61 | final StrokeCap strokeCap; 62 | 63 | @override 64 | void paint(Canvas canvas, Size size) { 65 | final paint = Paint() 66 | ..strokeWidth = strokeWidth 67 | ..strokeCap = strokeCap 68 | ..style = PaintingStyle.stroke; 69 | 70 | var offset = _DashOffset( 71 | containerSize: size, 72 | strokeWidth: strokeWidth, 73 | dashSize: dashSize, 74 | gapSize: gapSize, 75 | axis: direction, 76 | ); 77 | 78 | while (offset.hasNext) { 79 | // draw dash 80 | paint.color = color; 81 | canvas.drawLine( 82 | offset, 83 | offset.translateDashSize(), 84 | paint, 85 | ); 86 | offset = offset.translateDashSize(); 87 | 88 | // draw gap 89 | if (gapColor != Colors.transparent) { 90 | paint.color = gapColor; 91 | canvas.drawLine( 92 | offset, 93 | offset.translateGapSize(), 94 | paint, 95 | ); 96 | } 97 | offset = offset.translateGapSize(); 98 | } 99 | } 100 | 101 | @override 102 | bool shouldRepaint(DashedLinePainter oldDelegate) { 103 | return direction != oldDelegate.direction || 104 | color != oldDelegate.color || 105 | gapColor != oldDelegate.gapColor || 106 | dashSize != oldDelegate.dashSize || 107 | gapSize != oldDelegate.gapSize || 108 | strokeWidth != oldDelegate.strokeWidth || 109 | strokeCap != oldDelegate.strokeCap; 110 | } 111 | } 112 | 113 | class _DashOffset extends Offset { 114 | factory _DashOffset({ 115 | required Size containerSize, 116 | required double strokeWidth, 117 | required double dashSize, 118 | required double gapSize, 119 | required Axis axis, 120 | }) { 121 | return _DashOffset._( 122 | dx: axis == Axis.vertical ? containerSize.width / 2 : 0, 123 | dy: axis == Axis.vertical ? 0 : containerSize.height / 2, 124 | strokeWidth: strokeWidth, 125 | containerSize: containerSize, 126 | dashSize: dashSize, 127 | gapSize: gapSize, 128 | axis: axis, 129 | ); 130 | } 131 | 132 | const _DashOffset._({ 133 | required double dx, 134 | required double dy, 135 | required this.strokeWidth, 136 | required this.containerSize, 137 | required this.dashSize, 138 | required this.gapSize, 139 | required this.axis, 140 | }) : super(dx, dy); 141 | 142 | final Size containerSize; 143 | final double strokeWidth; 144 | final double dashSize; 145 | final double gapSize; 146 | final Axis axis; 147 | 148 | double get offset { 149 | if (axis == Axis.vertical) { 150 | return dy; 151 | } else { 152 | return dx; 153 | } 154 | } 155 | 156 | bool get hasNext { 157 | if (axis == Axis.vertical) { 158 | return offset < containerSize.height; 159 | } else { 160 | return offset < containerSize.width; 161 | } 162 | } 163 | 164 | _DashOffset translateDashSize() { 165 | return _translateDirectionally(dashSize); 166 | } 167 | 168 | _DashOffset translateGapSize() { 169 | return _translateDirectionally(gapSize + strokeWidth); 170 | } 171 | 172 | _DashOffset _translateDirectionally(double offset) { 173 | if (axis == Axis.vertical) { 174 | return translate(0, offset) as _DashOffset; 175 | } else { 176 | return translate(offset, 0) as _DashOffset; 177 | } 178 | } 179 | 180 | @override 181 | Offset translate(double translateX, double translateY) { 182 | double dx, dy; 183 | if (axis == Axis.vertical) { 184 | dx = this.dx; 185 | dy = this.dy + translateY; 186 | } else { 187 | dx = this.dx + translateX; 188 | dy = this.dy; 189 | } 190 | return copyWith( 191 | dx: min(dx, containerSize.width), 192 | dy: min(dy, containerSize.height), 193 | ); 194 | } 195 | 196 | _DashOffset copyWith({ 197 | double? dx, 198 | double? dy, 199 | Size? containerSize, 200 | double? strokeWidth, 201 | double? dashSize, 202 | double? gapSize, 203 | Axis? axis, 204 | }) { 205 | return _DashOffset._( 206 | dx: dx ?? this.dx, 207 | dy: dy ?? this.dy, 208 | containerSize: containerSize ?? this.containerSize, 209 | strokeWidth: strokeWidth ?? this.strokeWidth, 210 | dashSize: dashSize ?? this.dashSize, 211 | gapSize: gapSize ?? this.gapSize, 212 | axis: axis ?? this.axis, 213 | ); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /lib/src/timeline_node.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | 4 | import 'connectors.dart'; 5 | import 'indicators.dart'; 6 | import 'timeline_theme.dart'; 7 | import 'util.dart'; 8 | 9 | /// [TimelineTile]'s timeline node 10 | mixin TimelineTileNode on Widget { 11 | /// {@template timelines.node.position} 12 | /// If this is null, then the [TimelineThemeData.nodePosition] is used. 13 | /// {@endtemplate} 14 | double? get position; 15 | double getEffectivePosition(BuildContext context) { 16 | return position ?? TimelineTheme.of(context).nodePosition; 17 | } 18 | } 19 | 20 | /// A widget that displays indicator and two connectors. 21 | /// 22 | /// The [indicator] displayed between the [startConnector] and [endConnector] 23 | class TimelineNode extends StatelessWidget with TimelineTileNode { 24 | /// Creates a timeline node. 25 | /// 26 | /// The [indicatorPosition] must be null or a value between 0 and 1. 27 | const TimelineNode({ 28 | Key? key, 29 | this.direction, 30 | this.startConnector, 31 | this.endConnector, 32 | this.indicator = const ContainerIndicator(), 33 | this.indicatorPosition, 34 | this.position, 35 | this.overlap, 36 | }) : assert(indicatorPosition == null || 37 | 0 <= indicatorPosition && indicatorPosition <= 1), 38 | super(key: key); 39 | 40 | /// Creates a timeline node that connects the dot indicator in a solid line. 41 | TimelineNode.simple({ 42 | Key? key, 43 | Axis? direction, 44 | Color? color, 45 | double? lineThickness, 46 | double? nodePosition, 47 | double? indicatorPosition, 48 | double? indicatorSize, 49 | Widget? indicatorChild, 50 | double? indent, 51 | double? endIndent, 52 | bool drawStartConnector = true, 53 | bool drawEndConnector = true, 54 | bool? overlap, 55 | }) : this( 56 | key: key, 57 | direction: direction, 58 | startConnector: drawStartConnector 59 | ? SolidLineConnector( 60 | direction: direction, 61 | color: color, 62 | thickness: lineThickness, 63 | indent: indent, 64 | endIndent: endIndent, 65 | ) 66 | : null, 67 | endConnector: drawEndConnector 68 | ? SolidLineConnector( 69 | direction: direction, 70 | color: color, 71 | thickness: lineThickness, 72 | indent: indent, 73 | endIndent: endIndent, 74 | ) 75 | : null, 76 | indicator: DotIndicator( 77 | child: indicatorChild, 78 | position: indicatorPosition, 79 | size: indicatorSize, 80 | color: color, 81 | ), 82 | indicatorPosition: indicatorPosition, 83 | position: nodePosition, 84 | overlap: overlap, 85 | ); 86 | 87 | /// {@macro timelines.direction} 88 | final Axis? direction; 89 | 90 | /// The connector of the start edge of this node 91 | final Widget? startConnector; 92 | 93 | /// The connector of the end edge of this node 94 | final Widget? endConnector; 95 | 96 | /// The indicator of the node 97 | final Widget indicator; 98 | 99 | /// The position of a indicator between the two connectors. 100 | /// 101 | /// {@macro timelines.indicator.position} 102 | final double? indicatorPosition; 103 | 104 | /// A position of timeline node between both two contents. 105 | /// 106 | /// {@macro timelines.node.position} 107 | @override 108 | final double? position; 109 | 110 | /// Determine whether each connectors and indicator will overlap. 111 | /// 112 | /// When each connectors overlap, they are drawn from the center offset of the 113 | /// indicator. 114 | final bool? overlap; 115 | 116 | double _getEffectiveIndicatorPosition(BuildContext context) { 117 | var indicatorPosition = this.indicatorPosition; 118 | indicatorPosition ??= (indicator is PositionedIndicator) 119 | ? (indicator as PositionedIndicator).getEffectivePosition(context) 120 | : TimelineTheme.of(context).indicatorPosition; 121 | return indicatorPosition; 122 | } 123 | 124 | bool _getEffectiveOverlap(BuildContext context) { 125 | var overlap = this.overlap ?? TimelineTheme.of(context).nodeItemOverlap; 126 | return overlap; 127 | } 128 | 129 | @override 130 | Widget build(BuildContext context) { 131 | final direction = this.direction ?? TimelineTheme.of(context).direction; 132 | final overlap = _getEffectiveOverlap(context); 133 | // TODO: support both flex and logical pixel 134 | final indicatorFlex = _getEffectiveIndicatorPosition(context); 135 | Widget line = indicator; 136 | final lineItems = [ 137 | if (indicatorFlex > 0) 138 | Flexible( 139 | flex: (indicatorFlex * kFlexMultiplier).toInt(), 140 | child: startConnector ?? TransparentConnector(), 141 | ), 142 | if (!overlap) indicator, 143 | if (indicatorFlex < 1) 144 | Flexible( 145 | flex: ((1 - indicatorFlex) * kFlexMultiplier).toInt(), 146 | child: endConnector ?? TransparentConnector(), 147 | ), 148 | ]; 149 | 150 | switch (direction) { 151 | case Axis.vertical: 152 | line = Column( 153 | mainAxisSize: MainAxisSize.min, 154 | children: lineItems, 155 | ); 156 | break; 157 | case Axis.horizontal: 158 | line = Row( 159 | mainAxisSize: MainAxisSize.min, 160 | children: lineItems, 161 | ); 162 | break; 163 | } 164 | 165 | Widget result; 166 | if (overlap) { 167 | Widget positionedIndicator = indicator; 168 | final positionedIndicatorItems = [ 169 | if (indicatorFlex > 0) 170 | Flexible( 171 | flex: (indicatorFlex * kFlexMultiplier).toInt(), 172 | child: TransparentConnector(), 173 | ), 174 | indicator, 175 | Flexible( 176 | flex: ((1 - indicatorFlex) * kFlexMultiplier).toInt(), 177 | child: TransparentConnector(), 178 | ), 179 | ]; 180 | 181 | switch (direction) { 182 | case Axis.vertical: 183 | positionedIndicator = Column( 184 | mainAxisSize: MainAxisSize.min, 185 | children: positionedIndicatorItems, 186 | ); 187 | break; 188 | case Axis.horizontal: 189 | positionedIndicator = Row( 190 | mainAxisSize: MainAxisSize.min, 191 | children: positionedIndicatorItems, 192 | ); 193 | break; 194 | } 195 | 196 | result = Stack( 197 | alignment: Alignment.center, 198 | children: [ 199 | line, 200 | positionedIndicator, 201 | ], 202 | ); 203 | } else { 204 | result = line; 205 | } 206 | 207 | if (TimelineTheme.of(context).direction != direction) { 208 | result = TimelineTheme( 209 | data: TimelineTheme.of(context).copyWith( 210 | direction: direction, 211 | ), 212 | child: result, 213 | ); 214 | } 215 | 216 | return result; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /lib/src/timeline_theme.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'connector_theme.dart'; 8 | import 'indicator_theme.dart'; 9 | import 'timelines.dart'; 10 | 11 | /// Applies a theme to descendant timeline widgets. 12 | /// 13 | /// A theme describes the colors and typographic choices of an application. 14 | /// 15 | /// Descendant widgets obtain the current theme's [TimelineThemeData] object 16 | /// using [TimelineTheme.of]. When a widget uses [TimelineTheme.of], it is 17 | /// automatically rebuilt if the theme later changes, so that the changes can be 18 | /// applied. 19 | /// 20 | /// See also: 21 | /// 22 | /// * [TimelineThemeData], which describes the actual configuration of a theme. 23 | class TimelineTheme extends StatelessWidget { 24 | /// Applies the given theme [data] to [child]. 25 | /// 26 | /// The [data] and [child] arguments must not be null. 27 | const TimelineTheme({ 28 | Key? key, 29 | required this.data, 30 | required this.child, 31 | }) : super(key: key); 32 | 33 | /// Specifies the direction for descendant widgets. 34 | final TimelineThemeData data; 35 | 36 | /// The widget below this widget in the tree. 37 | /// 38 | /// {@macro flutter.widgets.child} 39 | final Widget child; 40 | 41 | static final TimelineThemeData _kFallbackTheme = TimelineThemeData.fallback(); 42 | 43 | /// The data from the closest [TimelineTheme] instance that encloses the given 44 | /// context. 45 | /// 46 | /// Defaults to [new ThemeData.fallback] if there is no [Theme] in the given 47 | /// build context. 48 | /// 49 | /// When the [TimelineTheme] is actually created in the same `build` function 50 | /// (possibly indirectly, e.g. as part of a [Timeline]), the `context` 51 | /// argument to the `build` function can't be used to find the [TimelineTheme] 52 | /// (since it's "above" the widget being returned). In such cases, the 53 | /// following technique with a [Builder] can be used to provide a new scope 54 | /// with a [BuildContext] that is "under" the [TimelineTheme]: 55 | /// 56 | /// ```dart 57 | /// @override 58 | /// Widget build(BuildContext context) { 59 | /// // TODO: replace to Timeline 60 | /// return TimelineTheme( 61 | /// data: TimelineThemeData.vertical(), 62 | /// child: Builder( 63 | /// // Create an inner BuildContext so that we can refer to the Theme with TimelineTheme.of(). 64 | /// builder: (BuildContext context) { 65 | /// return Center( 66 | /// child: TimelineNode( 67 | /// direction: TimelineTheme.of(context).direction, 68 | /// child: Text('Example'), 69 | /// ), 70 | /// ); 71 | /// }, 72 | /// ), 73 | /// ); 74 | /// } 75 | /// ``` 76 | static TimelineThemeData of(BuildContext context) { 77 | final inheritedTheme = 78 | context.dependOnInheritedWidgetOfExactType<_InheritedTheme>(); 79 | return inheritedTheme?.theme.data ?? _kFallbackTheme; 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | return _InheritedTheme( 85 | theme: this, 86 | child: IndicatorTheme( 87 | data: data.indicatorTheme, 88 | child: child, 89 | ), 90 | ); 91 | } 92 | 93 | @override 94 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 95 | super.debugFillProperties(properties); 96 | properties.add( 97 | DiagnosticsProperty('data', data, showName: false)); 98 | } 99 | } 100 | 101 | class _InheritedTheme extends InheritedTheme { 102 | const _InheritedTheme({ 103 | Key? key, 104 | required this.theme, 105 | required Widget child, 106 | }) : super(key: key, child: child); 107 | 108 | final TimelineTheme theme; 109 | 110 | @override 111 | Widget wrap(BuildContext context, Widget child) { 112 | final ancestorTheme = 113 | context.findAncestorWidgetOfExactType<_InheritedTheme>(); 114 | return identical(this, ancestorTheme) 115 | ? child 116 | : TimelineTheme(data: theme.data, child: child); 117 | } 118 | 119 | @override 120 | bool updateShouldNotify(_InheritedTheme old) => theme.data != old.theme.data; 121 | } 122 | 123 | /// Defines the configuration of the overall visual [TimelineTheme] for a 124 | /// [Timeline] or a widget subtree within the app. 125 | /// 126 | /// The [Timeline] theme property can be used to configure the appearance of the 127 | /// entire timeline. Widget subtree's within an timeline can override the 128 | /// timeline's theme by including a [TimelineTheme] widget at the top of the 129 | /// subtree. 130 | /// 131 | /// Widgets whose appearance should align with the overall theme can obtain the 132 | /// current theme's configuration with [TimelineTheme.of]. 133 | /// 134 | /// The static [TimelineTheme.of] method finds the [TimelineThemeData] value 135 | /// specified for the nearest [BuildContext] ancestor. This lookup is 136 | /// inexpensive, essentially just a single HashMap access. It can sometimes be a 137 | /// little confusing because [TimelineTheme.of] can not see a [TimelineTheme] 138 | /// widget that is defined in the current build method's context. To overcome 139 | /// that, create a new custom widget for the subtree that appears below the new 140 | /// [TimelineTheme], or insert a widget that creates a new BuildContext, like 141 | /// [Builder]. 142 | /// 143 | /// {@tool snippet} 144 | /// In this example, the [Container] widget uses [Theme.of] to retrieve the 145 | /// color from the theme's [color] to draw an amber square. 146 | /// The [Builder] widget separates the parent theme's [BuildContext] from the 147 | /// child's [BuildContext]. 148 | /// 149 | /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/theme_data.png) 150 | /// 151 | /// ```dart 152 | /// TimelineTheme( 153 | /// data: TimelineThemeData( 154 | /// color: Colors.red, 155 | /// ), 156 | /// child: Builder( 157 | /// builder: (BuildContext context) { 158 | /// return Container( 159 | /// width: 100, 160 | /// height: 100, 161 | /// color: TimelineTheme.of(context).color, 162 | /// ); 163 | /// }, 164 | /// ), 165 | /// ) 166 | /// ``` 167 | /// {@end-tool} 168 | /// 169 | /// {@tool snippet} 170 | @immutable 171 | class TimelineThemeData with Diagnosticable { 172 | /// Create a [TimelineThemeData] that's used to configure a [TimelineTheme]. 173 | /// 174 | /// See also: 175 | /// 176 | /// * [TimelineThemeData.vertical], which creates a vertical direction 177 | /// TimelineThemeData. 178 | /// * [TimelineThemeData.horizontal], which creates a horizontal direction 179 | /// TimelineThemeData. 180 | factory TimelineThemeData({ 181 | Axis? direction, 182 | Color? color, 183 | double? nodePosition, 184 | bool? nodeItemOverlap, 185 | double? indicatorPosition, 186 | IndicatorThemeData? indicatorTheme, 187 | ConnectorThemeData? connectorTheme, 188 | }) { 189 | direction ??= Axis.vertical; 190 | color ??= Colors 191 | .blue; // TODO: Need to change the default color to the theme color? 192 | nodePosition ??= 0.5; 193 | nodeItemOverlap ??= false; 194 | indicatorPosition ??= 0.5; 195 | indicatorTheme ??= IndicatorThemeData(); 196 | connectorTheme ??= ConnectorThemeData(); 197 | return TimelineThemeData.raw( 198 | direction: direction, 199 | color: color, 200 | nodePosition: nodePosition, 201 | nodeItemOverlap: nodeItemOverlap, 202 | indicatorPosition: indicatorPosition, 203 | indicatorTheme: indicatorTheme, 204 | connectorTheme: connectorTheme, 205 | ); 206 | } 207 | 208 | /// The default direction theme. Same as [new TimelineThemeData.vertical]. 209 | /// 210 | /// This is used by [TimelineTheme.of] when no theme has been specified. 211 | factory TimelineThemeData.fallback() => TimelineThemeData.vertical(); 212 | 213 | /// Create a [TimelineThemeData] given a set of exact values. All the values 214 | /// must be specified. They all must also be non-null. 215 | /// 216 | /// This will rarely be used directly. It is used by [lerp] to create 217 | /// intermediate themes based on two themes created with the 218 | /// [new TimelineThemeData] constructor. 219 | const TimelineThemeData.raw({ 220 | required this.direction, 221 | required this.color, 222 | required this.nodePosition, 223 | required this.nodeItemOverlap, 224 | required this.indicatorPosition, 225 | required this.indicatorTheme, 226 | required this.connectorTheme, 227 | }); 228 | 229 | /// A default vertical theme. 230 | factory TimelineThemeData.vertical() => TimelineThemeData( 231 | direction: Axis.vertical, 232 | ); 233 | 234 | /// A default horizontal theme. 235 | factory TimelineThemeData.horizontal() => TimelineThemeData( 236 | direction: Axis.horizontal, 237 | ); 238 | 239 | /// {@macro timelines.direction} 240 | final Axis direction; 241 | 242 | /// The color for major parts of the timeline (indicator, connector, etc) 243 | final Color color; 244 | 245 | /// The position for [TimelineNode] in [TimelineTile]. 246 | /// 247 | /// Defaults to 0.5. 248 | final double nodePosition; 249 | 250 | /// Determine whether each connectors and indicator will overlap in 251 | /// [TimelineNode]. 252 | /// 253 | /// When each connectors overlap, they are drawn from the center offset of the 254 | /// indicator. 255 | final bool nodeItemOverlap; 256 | 257 | /// The position for indicator in [TimelineNode]. 258 | /// 259 | /// Defaults to 0.5. 260 | final double indicatorPosition; 261 | 262 | /// A theme for customizing the appearance and layout of 263 | /// [ThemedIndicatorComponent] widgets. 264 | final IndicatorThemeData indicatorTheme; 265 | 266 | /// A theme for customizing the appearance and layout of 267 | /// [ThemedConnectorComponent] widgets. 268 | final ConnectorThemeData connectorTheme; 269 | 270 | /// Creates a copy of this theme but with the given fields replaced with the 271 | /// new values. 272 | TimelineThemeData copyWith({ 273 | Axis? direction, 274 | Color? color, 275 | double? nodePosition, 276 | bool? nodeItemOverlap, 277 | double? indicatorPosition, 278 | IndicatorThemeData? indicatorTheme, 279 | ConnectorThemeData? connectorTheme, 280 | }) { 281 | return TimelineThemeData.raw( 282 | direction: direction ?? this.direction, 283 | color: color ?? this.color, 284 | nodePosition: nodePosition ?? this.nodePosition, 285 | nodeItemOverlap: nodeItemOverlap ?? this.nodeItemOverlap, 286 | indicatorPosition: indicatorPosition ?? this.indicatorPosition, 287 | indicatorTheme: indicatorTheme ?? this.indicatorTheme, 288 | connectorTheme: connectorTheme ?? this.connectorTheme, 289 | ); 290 | } 291 | 292 | /// Linearly interpolate between two themes. 293 | /// 294 | /// The arguments must not be null. 295 | /// 296 | /// {@macro dart.ui.shadow.lerp} 297 | static TimelineThemeData lerp( 298 | TimelineThemeData a, TimelineThemeData b, double t) { 299 | // Warning: make sure these properties are in the exact same order as in 300 | // hashValues() and in the raw constructor and in the order of fields in 301 | // the class and in the lerp() method. 302 | return TimelineThemeData.raw( 303 | direction: t < 0.5 ? a.direction : b.direction, 304 | color: Color.lerp(a.color, b.color, t)!, 305 | nodePosition: lerpDouble(a.nodePosition, b.nodePosition, t)!, 306 | nodeItemOverlap: t < 0.5 ? a.nodeItemOverlap : b.nodeItemOverlap, 307 | indicatorPosition: 308 | lerpDouble(a.indicatorPosition, b.indicatorPosition, t)!, 309 | indicatorTheme: 310 | IndicatorThemeData.lerp(a.indicatorTheme, b.indicatorTheme, t), 311 | connectorTheme: 312 | ConnectorThemeData.lerp(a.connectorTheme, b.connectorTheme, t), 313 | ); 314 | } 315 | 316 | @override 317 | bool operator ==(Object other) { 318 | if (other.runtimeType != runtimeType) return false; 319 | // Warning: make sure these properties are in the exact same order as in 320 | // hashValues() and in the raw constructor and in the order of fields in 321 | // the class and in the lerp() method. 322 | return other is TimelineThemeData && 323 | other.direction == direction && 324 | other.color == color && 325 | other.nodePosition == nodePosition && 326 | other.nodeItemOverlap == nodeItemOverlap && 327 | other.indicatorPosition == indicatorPosition && 328 | other.indicatorTheme == indicatorTheme && 329 | other.connectorTheme == connectorTheme; 330 | } 331 | 332 | @override 333 | int get hashCode { 334 | // Warning: For the sanity of the reader, please make sure these properties 335 | // are in the exact same order as in operator == and in the raw constructor 336 | // and in the order of fields in the class and in the lerp() method. 337 | final values = [ 338 | direction, 339 | color, 340 | nodePosition, 341 | nodeItemOverlap, 342 | indicatorPosition, 343 | indicatorTheme, 344 | connectorTheme, 345 | ]; 346 | return hashList(values); 347 | } 348 | 349 | @override 350 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 351 | super.debugFillProperties(properties); 352 | final defaultData = TimelineThemeData.fallback(); 353 | properties 354 | ..add(DiagnosticsProperty('direction', direction, 355 | defaultValue: defaultData.direction, level: DiagnosticLevel.debug)) 356 | ..add(ColorProperty('color', color, 357 | defaultValue: defaultData.color, level: DiagnosticLevel.debug)) 358 | ..add(DoubleProperty('nodePosition', nodePosition, 359 | defaultValue: defaultData.nodePosition, level: DiagnosticLevel.debug)) 360 | ..add(FlagProperty('nodeItemOverlap', 361 | value: nodeItemOverlap, ifTrue: 'overlap connector and indicator')) 362 | ..add(DoubleProperty('indicatorPosition', indicatorPosition, 363 | defaultValue: defaultData.indicatorPosition, 364 | level: DiagnosticLevel.debug)) 365 | ..add(DiagnosticsProperty( 366 | 'indicatorTheme', 367 | indicatorTheme, 368 | defaultValue: defaultData.indicatorTheme, 369 | level: DiagnosticLevel.debug, 370 | )) 371 | ..add(DiagnosticsProperty( 372 | 'connectorTheme', 373 | connectorTheme, 374 | defaultValue: defaultData.connectorTheme, 375 | level: DiagnosticLevel.debug, 376 | )); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /lib/src/timeline_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | 5 | import 'indicator_theme.dart'; 6 | import 'timeline_node.dart'; 7 | import 'timeline_theme.dart'; 8 | import 'util.dart'; 9 | 10 | /// Align the timeline node within the timeline tile. 11 | enum TimelineNodeAlign { 12 | /// Align [TimelineTile.node] to start side. 13 | start, 14 | 15 | /// Align [TimelineTile.node] to end side. 16 | end, 17 | 18 | /// Align according to the [TimelineTile.nodePosition]. 19 | basic, 20 | } 21 | 22 | /// A widget that displays timeline node and two contents. 23 | /// 24 | /// The [contents] are displayed on the end side, and the [oppositeContents] are 25 | /// displayed on the start side. 26 | /// The [node] is displayed between the two. 27 | class TimelineTile extends StatelessWidget { 28 | const TimelineTile({ 29 | Key? key, 30 | this.direction, 31 | required this.node, 32 | this.nodeAlign = TimelineNodeAlign.basic, 33 | this.nodePosition, 34 | this.contents, 35 | this.oppositeContents, 36 | this.mainAxisExtent, 37 | this.crossAxisExtent, 38 | }) : assert( 39 | nodeAlign == TimelineNodeAlign.basic || 40 | (nodeAlign != TimelineNodeAlign.basic && nodePosition == null), 41 | 'Cannot provide both a nodeAlign and a nodePosition', 42 | ), 43 | assert(nodePosition == null || nodePosition >= 0), 44 | super(key: key); 45 | 46 | /// {@template timelines.direction} 47 | /// The axis along which the timeline scrolls. 48 | /// {@endtemplate} 49 | final Axis? direction; 50 | 51 | /// A widget that displays indicator and two connectors. 52 | final Widget node; 53 | 54 | /// Align the [node] within the timeline tile. 55 | /// 56 | /// If try to use indicators with different sizes in each timeline tile, the 57 | /// timeline node may be broken. 58 | /// This can be prevented by set [IndicatorThemeData.size] to an appropriate 59 | /// size. 60 | /// 61 | /// If [nodeAlign] is not [TimelineNodeAlign.basic], then [nodePosition] is 62 | /// ignored. 63 | final TimelineNodeAlign nodeAlign; 64 | 65 | /// A position of [node] inside both two contents. 66 | /// 67 | /// {@macro timelines.node.position} 68 | final double? nodePosition; 69 | 70 | /// The contents to display inside the timeline tile. 71 | final Widget? contents; 72 | 73 | /// The contents to display on the opposite side of the [contents]. 74 | final Widget? oppositeContents; 75 | 76 | /// The extent of the child in the scrolling axis. 77 | /// If the scroll axis is vertical, this extent is the child's height. If the 78 | /// scroll axis is horizontal, this extent is the child's width. 79 | /// 80 | /// If non-null, forces the tile to have the given extent in the scroll 81 | /// direction. 82 | /// 83 | /// Specifying an [mainAxisExtent] is more efficient than letting the tile 84 | /// determine their own extent because the because it don't use the Intrinsic 85 | /// widget([IntrinsicHeight]/[IntrinsicWidth]) when building. 86 | final double? mainAxisExtent; 87 | 88 | /// The extent of the child in the non-scrolling axis. 89 | /// 90 | /// If the scroll axis is vertical, this extent is the child's width. If the 91 | /// scroll axis is horizontal, this extent is the child's height. 92 | final double? crossAxisExtent; 93 | 94 | double _getEffectiveNodePosition(BuildContext context) { 95 | if (nodeAlign == TimelineNodeAlign.start) return 0.0; 96 | if (nodeAlign == TimelineNodeAlign.end) return 1.0; 97 | var nodePosition = this.nodePosition; 98 | nodePosition ??= (node is TimelineTileNode) 99 | ? (node as TimelineTileNode).getEffectivePosition(context) 100 | : TimelineTheme.of(context).nodePosition; 101 | return nodePosition; 102 | } 103 | 104 | @override 105 | Widget build(BuildContext context) { 106 | // TODO: reduce direction check 107 | final direction = this.direction ?? TimelineTheme.of(context).direction; 108 | final nodeFlex = _getEffectiveNodePosition(context) * kFlexMultiplier; 109 | 110 | var minNodeExtent = TimelineTheme.of(context).indicatorTheme.size ?? 0.0; 111 | var items = [ 112 | if (nodeFlex > 0) 113 | Expanded( 114 | flex: nodeFlex.toInt(), 115 | child: Align( 116 | alignment: direction == Axis.vertical 117 | ? AlignmentDirectional.centerEnd 118 | : Alignment.bottomCenter, 119 | child: oppositeContents ?? SizedBox.shrink(), 120 | ), 121 | ), 122 | ConstrainedBox( 123 | constraints: BoxConstraints( 124 | minWidth: direction == Axis.vertical ? minNodeExtent : 0.0, 125 | minHeight: direction == Axis.vertical ? 0.0 : minNodeExtent, 126 | ), 127 | child: node, 128 | ), 129 | if (nodeFlex < kFlexMultiplier) 130 | Expanded( 131 | flex: (kFlexMultiplier - nodeFlex).toInt(), 132 | child: Align( 133 | alignment: direction == Axis.vertical 134 | ? AlignmentDirectional.centerStart 135 | : Alignment.topCenter, 136 | child: contents ?? SizedBox.shrink(), 137 | ), 138 | ), 139 | ]; 140 | 141 | var result; 142 | switch (direction) { 143 | case Axis.vertical: 144 | result = Row( 145 | mainAxisAlignment: MainAxisAlignment.center, 146 | children: items, 147 | ); 148 | 149 | if (mainAxisExtent != null) { 150 | result = SizedBox( 151 | width: crossAxisExtent, 152 | height: mainAxisExtent, 153 | child: result, 154 | ); 155 | } else { 156 | result = IntrinsicHeight( 157 | child: result, 158 | ); 159 | 160 | if (crossAxisExtent != null) { 161 | result = SizedBox( 162 | width: crossAxisExtent, 163 | child: result, 164 | ); 165 | } 166 | } 167 | break; 168 | case Axis.horizontal: 169 | result = Column( 170 | mainAxisAlignment: MainAxisAlignment.center, 171 | children: items, 172 | ); 173 | if (mainAxisExtent != null) { 174 | result = SizedBox( 175 | width: mainAxisExtent, 176 | height: crossAxisExtent, 177 | child: result, 178 | ); 179 | } else { 180 | result = IntrinsicWidth( 181 | child: result, 182 | ); 183 | 184 | if (crossAxisExtent != null) { 185 | result = SizedBox( 186 | height: crossAxisExtent, 187 | child: result, 188 | ); 189 | } 190 | } 191 | break; 192 | default: 193 | throw ArgumentError.value(direction, '$direction is invalid.'); 194 | } 195 | 196 | result = Align( 197 | child: result, 198 | ); 199 | 200 | if (TimelineTheme.of(context).direction != direction) { 201 | result = TimelineTheme( 202 | data: TimelineTheme.of(context).copyWith( 203 | direction: direction, 204 | ), 205 | child: result, 206 | ); 207 | } 208 | 209 | return result; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /lib/src/util.dart: -------------------------------------------------------------------------------- 1 | const kFlexMultiplier = 1000.0; 2 | -------------------------------------------------------------------------------- /lib/timelines.dart: -------------------------------------------------------------------------------- 1 | /// Widgets that make it easy to implement the timeline UI component. 2 | library timelines; 3 | 4 | export 'package:timelines/src/connector_theme.dart'; 5 | export 'package:timelines/src/connectors.dart'; 6 | export 'package:timelines/src/indicator_theme.dart'; 7 | export 'package:timelines/src/indicators.dart'; 8 | export 'package:timelines/src/timelines.dart'; 9 | export 'package:timelines/src/timeline_node.dart'; 10 | export 'package:timelines/src/timeline_theme.dart'; 11 | export 'package:timelines/src/timeline_tile.dart'; 12 | export 'package:timelines/src/timeline_tile_builder.dart'; 13 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: timelines 2 | version: 0.1.0 3 | description: A powerful & easy to use timeline package for Flutter. All UI components in this package are separate widgets. 4 | homepage: https://chulwoo.dev 5 | repository: https://github.com/chulwoo-park/timelines/ 6 | issue_tracker: https://github.com/chulwoo-park/timelines/issues 7 | 8 | environment: 9 | sdk: '>=2.12.0 <3.0.0' 10 | flutter: '>=1.17.0' 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | pedantic: ^1.11.0 20 | 21 | flutter: 22 | -------------------------------------------------------------------------------- /screenshots/complex_timeline_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/complex_timeline_node.png -------------------------------------------------------------------------------- /screenshots/connection_direction_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/connection_direction_after.png -------------------------------------------------------------------------------- /screenshots/connection_direction_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/connection_direction_before.png -------------------------------------------------------------------------------- /screenshots/container_indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/container_indicator.png -------------------------------------------------------------------------------- /screenshots/contents_align_alternating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/contents_align_alternating.png -------------------------------------------------------------------------------- /screenshots/contents_align_basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/contents_align_basic.png -------------------------------------------------------------------------------- /screenshots/contents_align_reverse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/contents_align_reverse.png -------------------------------------------------------------------------------- /screenshots/dashed_line_connector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/dashed_line_connector.png -------------------------------------------------------------------------------- /screenshots/decorated_line_connector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/decorated_line_connector.png -------------------------------------------------------------------------------- /screenshots/dot_indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/dot_indicator.png -------------------------------------------------------------------------------- /screenshots/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/example.png -------------------------------------------------------------------------------- /screenshots/outlined_dot_indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/outlined_dot_indicator.png -------------------------------------------------------------------------------- /screenshots/package_delivery_tracking.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/package_delivery_tracking.gif -------------------------------------------------------------------------------- /screenshots/process_timeline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/process_timeline.gif -------------------------------------------------------------------------------- /screenshots/simple_timeline_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/simple_timeline_node.png -------------------------------------------------------------------------------- /screenshots/solid_line_connector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/solid_line_connector.png -------------------------------------------------------------------------------- /screenshots/timeline_status.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/timeline_status.gif -------------------------------------------------------------------------------- /screenshots/timeline_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chulwoo-park/timelines/7be49590321c2b3f6e06d67c3b4b196c11cc23e8/screenshots/timeline_tile.png -------------------------------------------------------------------------------- /test/timelines_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | --------------------------------------------------------------------------------