├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _config.yml ├── assets └── image.png ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── assets │ └── image.png ├── lib │ └── main.dart ├── pubspec.yaml └── test │ └── widget_test.dart ├── lib ├── dotted_border.dart └── src │ ├── dashed_painter.dart │ ├── dotted_border_options.dart │ ├── enums.dart │ └── validators.dart ├── pubspec.yaml └── test ├── dotted_border_test.dart └── validators_test.dart /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: ajiloommen 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | pubspec.lock 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | /res 31 | lib/generated/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Generated.xcconfig 62 | **/ios/Flutter/app.flx 63 | **/ios/Flutter/app.zip 64 | **/ios/Flutter/flutter_assets/ 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | **/ios/Flutter/flutter_export_environment.sh 68 | 69 | # Exceptions to above rules. 70 | !**/ios/**/default.mode1v3 71 | !**/ios/**/default.mode2v3 72 | !**/ios/**/default.pbxuser 73 | !**/ios/**/default.perspectivev3 74 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 75 | 76 | 77 | ./pubspec.lock 78 | -------------------------------------------------------------------------------- /.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: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### [3.0.1] - 17 May, 2023 2 | - Format code 3 | 4 | ### [3.0.0] - 17 May, 2023 5 | - BREAKING changes to DottedBorder API 6 | 7 | ### [2.1.0] - 25 September, 2023 8 | - Add StackFit as a parameter 9 | 10 | ### [2.0.0+2] - 5 December, 2021 11 | - Use path_drawing: ^1.0.0 12 | 13 | ### [2.0.0+1] - 10 August, 2021 14 | - Fixed wrong placement for CircleBorder 15 | 16 | ### [2.0.0] - 17 April, 2021 17 | - Move null safety to stable and master 18 | 19 | ### [2.0.0-nullsafety.0] - 23 December, 2020 20 | - Migrated package to use null safety 21 | 22 | ### [1.0.6] - 16 July, 2020 23 | - Formatted code and added code comments 24 | 25 | ### [1.0.5] - 3 March, 2020 26 | 27 | - Changed Path property to a PathBuilder so that the size of the child widget is received as a callback 28 | 29 | ### [1.0.4] - 10 December, 2019 30 | 31 | - Added check to ensure invalid dash patterns are not entered in the dotted border widget 32 | 33 | ### [1.0.3] - 8 October, 2019 34 | 35 | - Added StrokeCap property 36 | 37 | ### [1.0.2] - 9 July, 2019 38 | 39 | - Added ability to let users specify their own custom paths as border 40 | 41 | ### [1.0.1] - 3 July, 2019 42 | 43 | - Added a basic Dart doc 44 | 45 | ### [1.0.0] - 3 July, 2019 46 | 47 | - Add more path options 48 | - Added dash pattern option 49 | 50 | ### [0.1.1] - 20 May, 2019 51 | 52 | - Added description 53 | 54 | ### [0.1.0] - 20 May, 2019 55 | 56 | - Draw rectangular border around any widget. 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ajil Oommen 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 | # Dotted Border 2 | 3 | [![pub package](https://img.shields.io/badge/pub-3.0.1-blue.svg)](https://pub.dev/packages/dotted_border) 4 | 5 | A flutter package to easily add dotted borders around widgets. 6 | 7 | ### Installing 8 | 9 | To use this package, add `dotted_border` as a dependency in your `pubspec.yaml` file. 10 | 11 | ### Usage 12 | 13 | Wrap `DottedBorder` widget around the child widget 14 | 15 | ```dart 16 | DottedBorder( 17 | child: Text( 18 | 'Rectangular Border', 19 | style: TextStyle( 20 | fontWeight: FontWeight.bold, 21 | ), 22 | ), 23 | ) 24 | ``` 25 | 26 | ### BorderTypes 27 | 28 | This package supports the following border options 29 | * CustomPath 30 | * RoundedRect 31 | * Rect 32 | * Circular 33 | * Oval 34 | 35 | #### Example 36 | 37 | ```dart 38 | DottedBorder( 39 | options: RectDottedBorderOptions( 40 | dashPattern: [10, 5], 41 | strokeWidth: 2, 42 | padding: EdgeInsets.all(16), 43 | ), 44 | child: Text( 45 | 'Rectangular Border', 46 | style: TextStyle( 47 | fontWeight: FontWeight.bold, 48 | ), 49 | ), 50 | ) 51 | ``` 52 | 53 | ### Dash Pattern 54 | 55 | You can also specify the Dash Sequence by passing in an Array of Doubles 56 | 57 | #### Example 58 | ```dart 59 | RectDottedBorderOptions( 60 | dashPattern: [10, 5], 61 | strokeWidth: 2, 62 | padding: EdgeInsets.all(16), 63 | ) 64 | ``` 65 | 66 | The above code block will render a dashed border with the following pattern: 67 | 68 | * 10 pixel wide dash 69 | * 5 pixel wide space 70 | 71 | ### Custom Path Border 72 | 73 | You can also specify a `customPath` property and it will draw it for you using the provided dash pattern. 74 | 75 | #### Example 76 | 77 | ```dart 78 | DottedBorder( 79 | options: CustomPathDottedBorderOptions( 80 | padding: const EdgeInsets.all(8), 81 | color: Colors.purple, 82 | strokeWidth: 2, 83 | dashPattern: [10, 5], 84 | customPath: (size) => Path() 85 | ..moveTo(0, size.height) 86 | ..relativeLineTo(size.width, 0), 87 | ), 88 | child: const Text( 89 | 'Custom Path Border', 90 | style: TextStyle( 91 | color: Colors.purple, 92 | fontWeight: FontWeight.bold, 93 | ), 94 | ), 95 | ) 96 | ``` 97 | 98 | #### Output 99 | 100 | ![Flutter dotted border image](assets/image.png?raw=true "Flutter Dotted Border Image" ) 101 | 102 | ### Credits 103 | 104 | * [Flutter Path Drawing](https://github.com/dnfield/flutter_path_drawing) - [Dan Field](https://github.com/dnfield) 105 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajilo297/Flutter-Dotted-Border/5952d002698c9ce19bfe8ee8ebdd7cc2ef65fa4e/assets/image.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | /build/ 29 | 30 | # Android related 31 | **/android/**/gradle-wrapper.jar 32 | **/android/.gradle 33 | **/android/captures/ 34 | **/android/gradlew 35 | **/android/gradlew.bat 36 | **/android/local.properties 37 | **/android/**/GeneratedPluginRegistrant.java 38 | 39 | # iOS/XCode related 40 | **/ios/**/*.mode1v3 41 | **/ios/**/*.mode2v3 42 | **/ios/**/*.moved-aside 43 | **/ios/**/*.pbxuser 44 | **/ios/**/*.perspectivev3 45 | **/ios/**/*sync/ 46 | **/ios/**/.sconsign.dblite 47 | **/ios/**/.tags* 48 | **/ios/**/.vagrant/ 49 | **/ios/**/DerivedData/ 50 | **/ios/**/Icon? 51 | **/ios/**/Pods/ 52 | **/ios/**/.symlinks/ 53 | **/ios/**/profile 54 | **/ios/**/xcuserdata 55 | **/ios/.generated/ 56 | **/ios/Flutter/App.framework 57 | **/ios/Flutter/Flutter.framework 58 | **/ios/Flutter/Generated.xcconfig 59 | **/ios/Flutter/app.flx 60 | **/ios/Flutter/app.zip 61 | **/ios/Flutter/flutter_assets/ 62 | **/ios/ServiceDefinitions.json 63 | **/ios/Runner/GeneratedPluginRegistrant.* 64 | /ios/Flutter/Flutter.podspec 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | 73 | android 74 | ios 75 | web 76 | windows -------------------------------------------------------------------------------- /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: "12fccda598477eddd19f93040a1dba24f915b9be" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 12fccda598477eddd19f93040a1dba24f915b9be 17 | base_revision: 12fccda598477eddd19f93040a1dba24f915b9be 18 | - platform: android 19 | create_revision: 12fccda598477eddd19f93040a1dba24f915b9be 20 | base_revision: 12fccda598477eddd19f93040a1dba24f915b9be 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | Sample application to demonstrate usage of `DottedBorder` package. 4 | 5 | ### Running the sample application 6 | 7 | Fetch dependencies 8 | 9 | ```shell 10 | flutter packages get 11 | ``` 12 | 13 | Run the application with 14 | 15 | ```shell 16 | flutter run 17 | ``` 18 | 19 | ### Output 20 | 21 | ![Flutter dotted border image](assets/image.png?raw=true "Flutter Dotted Border Image" ) -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /example/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajilo297/Flutter-Dotted-Border/5952d002698c9ce19bfe8ee8ebdd7cc2ef65fa4e/example/assets/image.png -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:dotted_border/dotted_border.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() => runApp(const MyApp()); 5 | 6 | class MyApp extends StatelessWidget { 7 | const MyApp({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) => MaterialApp( 11 | theme: ThemeData(useMaterial3: true), 12 | home: Scaffold( 13 | appBar: AppBar(title: const Text('Dotted Border')), 14 | body: SafeArea( 15 | child: ListView( 16 | children: const [ 17 | _RectDottedBorder(key: Key('rect_dotted_border')), 18 | _RoundedRectDottedBorder( 19 | key: Key('rounded_rect_dotted_border'), 20 | ), 21 | _OvalDottedBorder(key: Key('oval_dotted_border')), 22 | _CircleDottedBorder(key: Key('circle_dotted_border')), 23 | _CustomPathBorder(key: Key('custom_path_dotted_border')), 24 | _GradientBorder(key: Key('gradient_dotted_border')), 25 | ] 26 | .map( 27 | (e) => Padding( 28 | padding: const EdgeInsets.all(10), 29 | child: e, 30 | ), 31 | ) 32 | .toList(), 33 | ), 34 | ), 35 | ), 36 | ); 37 | } 38 | 39 | class _RectDottedBorder extends StatelessWidget { 40 | const _RectDottedBorder({super.key}); 41 | 42 | @override 43 | Widget build(BuildContext context) => const Center( 44 | child: DottedBorder( 45 | options: RectDottedBorderOptions( 46 | dashPattern: [10, 5], 47 | strokeWidth: 2, 48 | padding: EdgeInsets.all(16), 49 | ), 50 | child: Text( 51 | 'Rectangular Border', 52 | style: TextStyle( 53 | fontWeight: FontWeight.bold, 54 | ), 55 | ), 56 | ), 57 | ); 58 | } 59 | 60 | class _RoundedRectDottedBorder extends StatelessWidget { 61 | const _RoundedRectDottedBorder({super.key}); 62 | 63 | @override 64 | Widget build(BuildContext context) => const Center( 65 | child: DottedBorder( 66 | options: RoundedRectDottedBorderOptions( 67 | dashPattern: [10, 5], 68 | strokeWidth: 2, 69 | radius: Radius.circular(16), 70 | color: Colors.indigo, 71 | padding: EdgeInsets.all(16), 72 | ), 73 | child: Text( 74 | 'Rounded Rect Border', 75 | style: TextStyle( 76 | color: Colors.indigo, 77 | fontWeight: FontWeight.bold, 78 | ), 79 | ), 80 | ), 81 | ); 82 | } 83 | 84 | class _OvalDottedBorder extends StatelessWidget { 85 | const _OvalDottedBorder({super.key}); 86 | 87 | @override 88 | Widget build(BuildContext context) => const Center( 89 | child: DottedBorder( 90 | options: OvalDottedBorderOptions( 91 | dashPattern: [10, 5], 92 | strokeWidth: 2, 93 | color: Colors.green, 94 | padding: EdgeInsets.all(16), 95 | ), 96 | child: Text( 97 | 'Oval Border', 98 | style: TextStyle( 99 | color: Colors.green, 100 | fontWeight: FontWeight.bold, 101 | ), 102 | ), 103 | ), 104 | ); 105 | } 106 | 107 | class _CircleDottedBorder extends StatelessWidget { 108 | const _CircleDottedBorder({super.key}); 109 | 110 | @override 111 | Widget build(BuildContext context) => const Center( 112 | child: DottedBorder( 113 | options: CircularDottedBorderOptions( 114 | dashPattern: [10, 5], 115 | strokeWidth: 4, 116 | color: Colors.red, 117 | padding: EdgeInsets.all(4), 118 | ), 119 | child: Icon( 120 | Icons.person, 121 | size: 64, 122 | color: Colors.red, 123 | ), 124 | ), 125 | ); 126 | } 127 | 128 | class _CustomPathBorder extends StatelessWidget { 129 | const _CustomPathBorder({super.key}); 130 | 131 | @override 132 | Widget build(BuildContext context) => Center( 133 | child: DottedBorder( 134 | options: CustomPathDottedBorderOptions( 135 | padding: const EdgeInsets.all(8), 136 | color: Colors.purple, 137 | strokeWidth: 2, 138 | dashPattern: [10, 5], 139 | customPath: (size) => Path() 140 | ..moveTo(0, size.height) 141 | ..relativeLineTo(size.width, 0), 142 | ), 143 | child: const Text( 144 | 'Custom Path Border', 145 | style: TextStyle( 146 | color: Colors.purple, 147 | fontWeight: FontWeight.bold, 148 | ), 149 | ), 150 | ), 151 | ); 152 | } 153 | 154 | class _GradientBorder extends StatelessWidget { 155 | const _GradientBorder({super.key}); 156 | 157 | @override 158 | Widget build(BuildContext context) => const Center( 159 | child: DottedBorder( 160 | options: RoundedRectDottedBorderOptions( 161 | dashPattern: [10, 5], 162 | strokeWidth: 4, 163 | radius: Radius.circular(16), 164 | gradient: LinearGradient( 165 | begin: Alignment.topLeft, 166 | end: Alignment.bottomRight, 167 | colors: [ 168 | Colors.red, 169 | Colors.orange, 170 | Colors.yellow, 171 | Colors.green, 172 | Colors.blue, 173 | Colors.indigo, 174 | Colors.purple, 175 | ], 176 | ), 177 | padding: EdgeInsets.all(16), 178 | ), 179 | child: Text( 180 | 'Rainbow Border', 181 | style: TextStyle( 182 | color: Colors.indigo, 183 | fontWeight: FontWeight.bold, 184 | ), 185 | ), 186 | ), 187 | ); 188 | } 189 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | publish_to: none 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: '>=3.1.1 <4.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | dotted_border: 13 | path: ../ 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | flutter_lints: ^3.0.1 19 | 20 | 21 | flutter: 22 | uses-material-design: true -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dotted_border/dotted_border.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import '../lib/main.dart'; 6 | 7 | void main() { 8 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 9 | await tester.pumpWidget(MyApp()); 10 | expect(find.byType(FlutterLogo), findsOneWidget); 11 | expect(find.byType(DottedBorder), findsOneWidget); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/dotted_border.dart: -------------------------------------------------------------------------------- 1 | library dotted_border; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'src/dashed_painter.dart'; 6 | import 'src/dotted_border_options.dart'; 7 | 8 | export 'src/dotted_border_options.dart'; 9 | 10 | /// A widget that draws a dotted border around its child. 11 | /// 12 | /// The [child] is painted on top of the border. 13 | /// Customization of the border is achieved by specifying the desired [options]. 14 | class DottedBorder extends StatelessWidget { 15 | const DottedBorder({ 16 | super.key, 17 | required this.child, 18 | this.options = const RectDottedBorderOptions(), 19 | }); 20 | 21 | /// The widget below this widget in the tree. 22 | final Widget child; 23 | 24 | /// The dotted border options. 25 | final DottedBorderOptions options; 26 | 27 | /// Whether the border should ignore pointer events. 28 | final bool ignoring = true; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | final radius = switch (options) { 33 | RoundedRectDottedBorderOptions options => options.radius, 34 | _ => Radius.zero, 35 | }; 36 | 37 | final customPath = switch (options) { 38 | CustomPathDottedBorderOptions options => options.customPath, 39 | _ => null, 40 | }; 41 | 42 | return Stack( 43 | fit: options.stackFit, 44 | children: [ 45 | Padding(padding: options.padding, child: child), 46 | Positioned.fill( 47 | child: IgnorePointer( 48 | ignoring: ignoring, 49 | child: CustomPaint( 50 | painter: DashedPainter( 51 | padding: options.borderPadding, 52 | strokeWidth: options.strokeWidth, 53 | radius: radius, 54 | color: options.color, 55 | gradient: options.gradient, 56 | borderType: options.borderType, 57 | dashPattern: options.dashPattern, 58 | customPath: customPath, 59 | strokeCap: options.strokeCap, 60 | ), 61 | ), 62 | ), 63 | ), 64 | ], 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/dashed_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:dotted_border/dotted_border.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:path_drawing/path_drawing.dart' as pd hide CircularIntervalList; 4 | import 'package:path_drawing/path_drawing.dart' show CircularIntervalList; 5 | 6 | import 'enums.dart'; 7 | import 'validators.dart'; 8 | 9 | /// A painter that draws a dashed line. 10 | class DashedPainter extends CustomPainter { 11 | const DashedPainter({ 12 | this.strokeWidth = 2, 13 | this.dashPattern = const [3, 1], 14 | this.color = Colors.black, 15 | this.gradient, 16 | this.borderType, 17 | this.radius = const Radius.circular(0), 18 | this.strokeCap = StrokeCap.butt, 19 | this.customPath, 20 | this.padding = EdgeInsets.zero, 21 | }); 22 | 23 | /// The thickness of the dashed line 24 | final double strokeWidth; 25 | 26 | /// The dash pattern to be drawn. 27 | final List dashPattern; 28 | 29 | /// The color of the dashed line. 30 | final Color color; 31 | 32 | /// The gradient of the dashed line. Will override [color] if provided. 33 | final Gradient? gradient; 34 | 35 | /// The type of border to be drawn 36 | final BorderType? borderType; 37 | 38 | /// The radius of the rounded corners 39 | final Radius radius; 40 | 41 | /// The stroke cap of the dashed line 42 | final StrokeCap strokeCap; 43 | 44 | /// Provides the custom path to be drawn 45 | /// Will override [borderType] if provided. 46 | final PathBuilder? customPath; 47 | 48 | /// The padding between the dashed line and the child 49 | final EdgeInsets padding; 50 | 51 | @override 52 | void paint(Canvas canvas, Size originalSize) { 53 | final Size size; 54 | 55 | canvas.translate(padding.left, padding.top); 56 | size = Size( 57 | originalSize.width - padding.horizontal, 58 | originalSize.height - padding.vertical, 59 | ); 60 | 61 | Paint paint = 62 | Paint() 63 | ..strokeWidth = strokeWidth 64 | ..strokeCap = strokeCap 65 | ..style = PaintingStyle.stroke; 66 | 67 | if (gradient != null) { 68 | final rect = Offset.zero & size; 69 | paint.shader = gradient!.createShader(rect); 70 | } else { 71 | paint.color = color; 72 | } 73 | 74 | Path _path = customPath?.call(size) ?? _getPath(size); 75 | 76 | final result = Validators.validateDashPattern(dashPattern); 77 | if (result != null) { 78 | throw ArgumentError(result.message); 79 | } 80 | 81 | canvas.drawPath( 82 | pd.dashPath(_path, dashArray: CircularIntervalList(dashPattern)), 83 | paint, 84 | ); 85 | } 86 | 87 | /// Returns the path to be drawn based on the [borderType] 88 | Path _getPath(Size size) => switch (borderType) { 89 | BorderType.RRect => size.toRoundedRectangularPath(radius), 90 | BorderType.Oval => size.toOvalPath(), 91 | BorderType.Circle => size.toCirclePath(), 92 | _ => size.toRectangularPath(), 93 | }; 94 | 95 | @override 96 | bool shouldRepaint(DashedPainter oldDelegate) => 97 | oldDelegate.strokeWidth != this.strokeWidth || 98 | oldDelegate.dashPattern != this.dashPattern || 99 | oldDelegate.color != this.color || 100 | oldDelegate.gradient != this.gradient || 101 | oldDelegate.borderType != this.borderType || 102 | oldDelegate.radius != this.radius || 103 | oldDelegate.strokeCap != this.strokeCap || 104 | oldDelegate.customPath != this.customPath || 105 | oldDelegate.padding != this.padding; 106 | } 107 | 108 | /// Provides extension methods on [Size] to convert it to a path. 109 | extension _SizeToPathExtension on Size { 110 | /// Returns a rectangular path of the given size. 111 | Path toRectangularPath() => 112 | Path()..addRect(Rect.fromLTWH(0, 0, width, height)); 113 | 114 | /// Returns a rounded rectangular path of the given size and radius. 115 | Path toRoundedRectangularPath(Radius radius) => 116 | Path()..addRRect( 117 | RRect.fromRectAndRadius(Rect.fromLTWH(0, 0, width, height), radius), 118 | ); 119 | 120 | /// Returns an oval path of the given size. 121 | Path toOvalPath() => Path()..addOval(Rect.fromLTWH(0, 0, width, height)); 122 | 123 | /// Returns a circular path of the given size. 124 | Path toCirclePath() => 125 | Path()..addRRect( 126 | RRect.fromRectAndRadius( 127 | Rect.fromLTWH( 128 | width > shortestSide ? (width - shortestSide) / 2 : 0, 129 | height > shortestSide ? (height - shortestSide) / 2 : 0, 130 | shortestSide, 131 | shortestSide, 132 | ), 133 | Radius.circular(shortestSide / 2), 134 | ), 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /lib/src/dotted_border_options.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'enums.dart'; 4 | 5 | typedef PathBuilder = Path Function(Size size); 6 | 7 | /// Provides options for customising the dotted border. 8 | /// 9 | /// Allowed border options: 10 | /// - [CustomPathDottedBorderOptions] 11 | /// - [RoundedRectDottedBorderOptions] 12 | /// - [RectDottedBorderOptions] 13 | /// - [CircularDottedBorderOptions] 14 | /// - [OvalDottedBorderOptions] 15 | sealed class DottedBorderOptions { 16 | const DottedBorderOptions({ 17 | required this.borderType, 18 | this.padding = const EdgeInsets.all(2), 19 | this.borderPadding = EdgeInsets.zero, 20 | this.strokeWidth = 1, 21 | this.color = Colors.black, 22 | this.dashPattern = const [3, 1], 23 | this.strokeCap = StrokeCap.butt, 24 | this.stackFit = StackFit.loose, 25 | this.gradient, 26 | }); 27 | 28 | /// The padding between the dotted border and the child 29 | final EdgeInsets padding; 30 | 31 | /// The padding between the dotted border and parent widget 32 | final EdgeInsets borderPadding; 33 | 34 | /// The thickness of the dotted border 35 | final double strokeWidth; 36 | 37 | /// The color of the dotted border. 38 | /// Will be overridden by [gradient] if provided 39 | final Color color; 40 | 41 | /// The gradient of the dotted border. 42 | final Gradient? gradient; 43 | 44 | /// The dash pattern to be drawn. 45 | /// `[1, 1]` will draw a dash and a gap of 1 unit each. 46 | final List dashPattern; 47 | 48 | /// The [strokeCap] will determine the shape of the line endings for the 49 | /// border 50 | final StrokeCap strokeCap; 51 | 52 | /// The fit provided to the parent stack 53 | final StackFit stackFit; 54 | 55 | /// The type of border to be drawn 56 | /// - [BorderType.Circle] 57 | /// - [BorderType.RRect] 58 | /// - [BorderType.Rect] 59 | /// - [BorderType.Oval] 60 | /// - [null] for custom path 61 | final BorderType? borderType; 62 | } 63 | 64 | /// Provides options for drawing a dotted border with a custom path. 65 | final class CustomPathDottedBorderOptions extends DottedBorderOptions { 66 | const CustomPathDottedBorderOptions({ 67 | required this.customPath, 68 | super.borderPadding, 69 | super.padding, 70 | super.strokeWidth, 71 | super.color, 72 | super.gradient, 73 | super.dashPattern, 74 | super.stackFit, 75 | super.strokeCap, 76 | }) : super(borderType: null); 77 | 78 | /// The custom path to be drawn 79 | final PathBuilder customPath; 80 | } 81 | 82 | /// Provides options for drawing a dotted border with a rounded rectangle. 83 | final class RoundedRectDottedBorderOptions extends DottedBorderOptions { 84 | const RoundedRectDottedBorderOptions({ 85 | required this.radius, 86 | super.borderPadding, 87 | super.padding, 88 | super.strokeWidth, 89 | super.color, 90 | super.gradient, 91 | super.dashPattern, 92 | super.stackFit, 93 | super.strokeCap, 94 | }) : super(borderType: BorderType.RRect); 95 | 96 | /// The radius of the rounded rectangle 97 | final Radius radius; 98 | } 99 | 100 | /// Provides options for drawing a dotted border with a rectangle. 101 | final class RectDottedBorderOptions extends DottedBorderOptions { 102 | const RectDottedBorderOptions({ 103 | super.borderPadding, 104 | super.padding, 105 | super.strokeWidth, 106 | super.color, 107 | super.gradient, 108 | super.dashPattern, 109 | super.stackFit, 110 | super.strokeCap, 111 | }) : super(borderType: BorderType.Rect); 112 | } 113 | 114 | /// Provides options for drawing a dotted border with a circle. 115 | final class CircularDottedBorderOptions extends DottedBorderOptions { 116 | const CircularDottedBorderOptions({ 117 | super.borderPadding, 118 | super.padding, 119 | super.strokeWidth, 120 | super.color, 121 | super.gradient, 122 | super.dashPattern, 123 | super.stackFit, 124 | super.strokeCap, 125 | }) : super(borderType: BorderType.Circle); 126 | } 127 | 128 | /// Provides options for drawing a dotted border with an oval. 129 | final class OvalDottedBorderOptions extends DottedBorderOptions { 130 | const OvalDottedBorderOptions({ 131 | super.borderPadding, 132 | super.padding, 133 | super.strokeWidth, 134 | super.color, 135 | super.gradient, 136 | super.dashPattern, 137 | super.stackFit, 138 | super.strokeCap, 139 | }) : super(borderType: BorderType.Oval); 140 | } 141 | -------------------------------------------------------------------------------- /lib/src/enums.dart: -------------------------------------------------------------------------------- 1 | enum BorderType { Circle, RRect, Rect, Oval } 2 | 3 | enum InvalidDashPatternResult { 4 | nullDashPattern, 5 | emptyDashPattern, 6 | negativeDashPattern, 7 | zeroDashPattern; 8 | 9 | String get message => switch (this) { 10 | InvalidDashPatternResult.nullDashPattern => 'Dash pattern cannot be null.', 11 | InvalidDashPatternResult.emptyDashPattern => 12 | 'Dash pattern cannot be empty.', 13 | InvalidDashPatternResult.negativeDashPattern => 14 | 'Dash pattern cannot have negative values.', 15 | InvalidDashPatternResult.zeroDashPattern => 16 | 'Dash pattern cannot contain all zeros.', 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/validators.dart: -------------------------------------------------------------------------------- 1 | import 'enums.dart'; 2 | 3 | class Validators { 4 | /// Validates the given [dashPattern]. 5 | /// 6 | /// Returns `null` if the given [dashPattern] is valid. 7 | /// [dashPattern] is invalid if 8 | /// - it is null 9 | /// - it is empty 10 | /// - it has a negative element 11 | /// - it has a single element with value of 0 12 | static InvalidDashPatternResult? validateDashPattern( 13 | List? dashPattern, 14 | ) { 15 | final dashSet = dashPattern?.toSet(); 16 | if (dashSet == null) return InvalidDashPatternResult.nullDashPattern; 17 | if (dashSet.isEmpty) return InvalidDashPatternResult.emptyDashPattern; 18 | 19 | if (dashSet.any((element) => element < 0.0)) { 20 | return InvalidDashPatternResult.negativeDashPattern; 21 | } 22 | 23 | if (dashSet.length == 1 && dashSet.elementAt(0) == 0.0) { 24 | return InvalidDashPatternResult.zeroDashPattern; 25 | } 26 | 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dotted_border 2 | description: A flutter package to let users easily add a dotted border around any widget. 3 | version: 3.0.1 4 | homepage: https://github.com/ajilo297/Flutter-Dotted-Border 5 | 6 | environment: 7 | sdk: ^3.7.2 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | path_drawing: ^1.0.0 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | -------------------------------------------------------------------------------- /test/dotted_border_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:dotted_border/dotted_border.dart'; 5 | 6 | void main() { 7 | testWidgets('Run test', (tester) async { 8 | await tester.pumpWidget( 9 | MaterialApp(home: Scaffold(body: DottedBorder(child: Text('test')))), 10 | ); 11 | 12 | expect(find.byType(DottedBorder), findsOneWidget); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/validators_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dotted_border/src/enums.dart'; 2 | import 'package:dotted_border/src/validators.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('Dash pattern validator', () { 7 | test('returns nullDashPattern if dash pattern is null', () { 8 | final pattern = null; 9 | final result = Validators.validateDashPattern(pattern); 10 | expect(result, InvalidDashPatternResult.nullDashPattern); 11 | }); 12 | 13 | test('returns emptyDashPattern if dash pattern is empty', () { 14 | final pattern = []; 15 | final result = Validators.validateDashPattern(pattern); 16 | expect(result, InvalidDashPatternResult.emptyDashPattern); 17 | }); 18 | 19 | test('returns negativeDashPattern if dash pattern has negative values', () { 20 | final pattern = [-1.0, 0.0, 1.0]; 21 | final result = Validators.validateDashPattern(pattern); 22 | expect(result, InvalidDashPatternResult.negativeDashPattern); 23 | }); 24 | 25 | test('returns zeroDashPattern if dash pattern has all zeros', () { 26 | final pattern = [0.0, 0.0, 0.0]; 27 | final result = Validators.validateDashPattern(pattern); 28 | expect(result, InvalidDashPatternResult.zeroDashPattern); 29 | }); 30 | 31 | test('returns null if dash pattern is valid', () { 32 | final pattern = [1.0, 2.0, 3.0]; 33 | final result = Validators.validateDashPattern(pattern); 34 | expect(result, null); 35 | }); 36 | }); 37 | } 38 | --------------------------------------------------------------------------------