├── .github ├── FUNDING.yml └── workflows │ └── dart.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc └── signature.png ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── dev │ │ │ │ │ └── basecontrol │ │ │ │ │ ├── example │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ └── signature_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 │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── 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 │ ├── main.dart │ └── scroll_test.dart ├── pubspec.yaml └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ └── Icon-512.png │ ├── index.html │ └── manifest.json ├── lib ├── signature.dart └── src │ ├── signature_control.dart │ ├── signature_paint.dart │ ├── signature_painter.dart │ ├── signature_view.dart │ └── utils.dart ├── pubspec.yaml └── test └── signature_control_test.dart /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [RomanBase] 2 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Dart 7 | 8 | on: 9 | push: 10 | branches: [ master ] 11 | pull_request: 12 | branches: [ master ] 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | # Note: This workflow uses the latest stable version of the Dart SDK. 22 | # You can specify other versions if desired, see documentation here: 23 | # https://github.com/dart-lang/setup-dart/blob/main/README.md 24 | # - uses: dart-lang/setup-dart@v1 25 | - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603 26 | 27 | - name: Install dependencies 28 | run: dart pub get 29 | 30 | # Uncomment this step to verify the use of 'dart format' on each commit. 31 | # - name: Verify formatting 32 | # run: dart format --output=none --set-exit-if-changed . 33 | 34 | # Consider passing '--fatal-infos' for slightly stricter analysis. 35 | - name: Analyze project source 36 | run: dart analyze 37 | 38 | # Your project will need to have tests in test/ and a dependency on 39 | # package:test for this step to succeed. Note that Flutter projects will 40 | # want to change this to 'flutter test'. 41 | - name: Run tests 42 | run: dart test 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | pubspec.lock 33 | 34 | # Android related 35 | **/android/**/gradle-wrapper.jar 36 | **/android/.gradle 37 | **/android/captures/ 38 | **/android/gradlew 39 | **/android/gradlew.bat 40 | **/android/local.properties 41 | **/android/**/GeneratedPluginRegistrant.java 42 | 43 | # iOS/XCode related 44 | **/ios/**/*.mode1v3 45 | **/ios/**/*.mode2v3 46 | **/ios/**/*.moved-aside 47 | **/ios/**/*.pbxuser 48 | **/ios/**/*.perspectivev3 49 | **/ios/**/*sync/ 50 | **/ios/**/.sconsign.dblite 51 | **/ios/**/.tags* 52 | **/ios/**/.vagrant/ 53 | **/ios/**/DerivedData/ 54 | **/ios/**/Icon? 55 | **/ios/**/Pods/ 56 | **/ios/**/.symlinks/ 57 | **/ios/**/profile 58 | **/ios/**/xcuserdata 59 | **/ios/.generated/ 60 | **/ios/Flutter/App.framework 61 | **/ios/Flutter/Flutter.framework 62 | **/ios/Flutter/Flutter.podspec 63 | **/ios/Flutter/Generated.xcconfig 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/ServiceDefinitions.json 69 | **/ios/Runner/GeneratedPluginRegistrant.* 70 | 71 | # Exceptions to above rules. 72 | !**/ios/**/default.mode1v3 73 | !**/ios/**/default.mode2v3 74 | !**/ios/**/default.pbxuser 75 | !**/ios/**/default.perspectivev3 76 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 77 | -------------------------------------------------------------------------------- /.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: 0b8abb4724aa590dd0f429683339b1e045a1594d 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.0.3] - Updated Gesture Recognizer 2 | Now can specify input type - `PointerDeviceKind`. 3 | ## [3.0.1] - Fit 4 | Resolve vertices scaling with `fit` flag in export.\ 5 | Rename some properties to unify naming across library. 6 | ## [3.0.0] - Dependency 7 | Removed dependency on `flutter_svg` and removed `HandSignatureView`. 8 | ## [2.3.0] - Import/Export current state (map/json) 9 | Refactor `HandSignaturePainterView` to `HandSignature` 10 | ## [2.2.0] - SVG wrap option 11 | ## [2.1.1] - Ability to export exact image 12 | toPicture and toImage now contains **fit** property. 13 | ## [2.1.0] - Custom Gesture Recognizer 14 | New `GestureRecognizer` based on `OneSequenceGestureRecognizer` that allows just one pointer and handles all pointer updates. 15 | All previous Recognizers have been removed. 16 | ## [2.0.0] - Nullsafety 17 | Minimum Dart SDK 2.12.0 18 | ## [0.6.3] - Scroll 19 | Added `TapGestureDetector` and current `PanGestureDetector` has been modified to support drawing in `ScrollView`.\ 20 | Also pointer callbacks are now exposed to detect **start** and **end** of drawing. 21 | ## [0.6.1] - Shape, Arc, Line 22 | Draw line as single shape (huge performance update).\ 23 | Selection of 3 draw styles (shape, arc, line). Arc is still nicest, but has performance issues..\ 24 | `SignatureDrawType.shape` is now default draw and export style. 25 | ## [0.5.1] - Dot 26 | Support dot drawing based on last line size.\ 27 | Minor performance updates. 28 | ## [0.5.0] - Alpha version of signature pad. 29 | Signature pad for smooth and real hand signatures. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 RomanBase 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A Flutter plugin providing Signature Pad for drawing smooth signatures. Library is written in pure Dart/Flutter environment to provide support for all platforms..\ 2 | Easy to use library with variety of draw and export settings. Also supports SVG files. 3 | 4 | ![Structure](https://raw.githubusercontent.com/RomanBase/hand_signature/master/doc/signature.png) 5 | 6 | Signature pad drawing is based on Cubic Bézier curves.\ 7 | Offers to choose between performance and beauty mode. 8 | 9 | --- 10 | 11 | **Usage** 12 | ```dart 13 | import 'package:hand_signature/signature.dart'; 14 | ``` 15 | 16 | With **HandSignatureControl** and **HandSignature** is possible to tweak some drawing aspects like stroke width, smoothing ratio or velocity weight. 17 | ```dart 18 | final control = HandSignatureControl( 19 | threshold: 3.0, 20 | smoothRatio: 0.65, 21 | velocityRange: 2.0, 22 | ); 23 | 24 | final widget = HandSignature( 25 | control: control, 26 | color: Colors.blueGrey, 27 | width: 1.0, 28 | maxWidth: 10.0, 29 | type: SignatureDrawType.shape, 30 | ); 31 | ``` 32 | 33 | **HandSignatureControl** sets up 'math' to control input touches and handles control points of signature curve. 34 | - threshold: (LP) controls minimal distance between two points - higher distance creates smoother curve, but less precise. Higher distance also creates bigger input draw lag. 35 | - smoothRatio: (0 - 1) controls how smooth curve will be - higher ratio creates smoother curve, but less precise. In most of cases are best results with values between 0.5 - 0.75. 36 | - velocityRange: (LP per millisecond) controls curve size based on distance and duration between two points. Thin line - fast move, thick line - slow move. With higher velocityRange user must swing faster to draw thinner line. 37 | - reverseVelocity: swaps stroke width. Thin line - slow move, thick line - fast move. Simply swaps min/max size based on velocity. 38 | 39 | **HandSignature** sets up visual style of signature curve. 40 | - control: processes input, handles math and stores raw data. 41 | - color: just color of line. 42 | - strokeWidth: minimal width of line. Width at maximum swing speed (clamped by velocityRange). 43 | - maxStrokeWidth: maximum width of line. Width at slowest swing speed. 44 | - type: draw type of curve. Default and main draw type is **shape** - not so nice as **arc**, but has better performance. And **line** is simple path with uniform stroke width. 45 | - line: basic Bezier line with best performance. 46 | - shape: like Ink drawn signature with still pretty good performance. 47 | - arc: beauty mode for Ink styled signature. 48 | --- 49 | 50 | **Export**\ 51 | Properties, like canvas size, stroke min/max width and color can be modified during export.\ 52 | There are more ways and more formats how to export signature, most used ones are **svg** and **png** formats. 53 | ```dart 54 | final control = HandSignatureControl(); 55 | 56 | final svg = control.toSvg(); 57 | final png = control.toImage(); 58 | final json = control.toMap(); 59 | 60 | control.importData(json); 61 | ``` 62 | **Svg**: SignatureDrawType **shape** generates reasonably small file and is read well by all programs. On the other side **arc** generates really big svg file and some programs can have hard times handling so much objects. **Line** is simple Bezier Curve.\ 63 | **Image**: Export to image supports **ImageByteFormat** and provides png or raw rgba data.\ 64 | **Json/Map**: Exports current state - raw data that can be used later to restore state. 65 | 66 | **Parsing and drawing saved SVG**\ 67 | Exported **svg** String is possible to display in another lib like: [flutter_svg](https://pub.dev/packages/flutter_svg). -------------------------------------------------------------------------------- /doc/signature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/doc/signature.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 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | -------------------------------------------------------------------------------- /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: 0b8abb4724aa590dd0f429683339b1e045a1594d 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # signature_example 2 | 3 | A new Flutter application. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /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 33 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | applicationId "dev.basecontrol.signature_example" 41 | minSdkVersion 23 42 | targetSdkVersion 33 43 | versionCode flutterVersionCode.toInteger() 44 | versionName flutterVersionName 45 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 46 | } 47 | 48 | buildTypes { 49 | release { 50 | signingConfig signingConfigs.debug 51 | } 52 | } 53 | } 54 | 55 | flutter { 56 | source '../..' 57 | } 58 | 59 | dependencies { 60 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 61 | testImplementation 'junit:junit:4.12' 62 | androidTestImplementation 'androidx.test:runner:1.1.1' 63 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 64 | } 65 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/dev/basecontrol/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.basecontrol.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/dev/basecontrol/signature_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.basecontrol.signature_example 2 | 3 | import androidx.annotation.NonNull; 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 10 | GeneratedPluginRegistrant.registerWith(flutterEngine); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.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 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=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-7.6.1-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /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 | 10.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 97C146F11CF9000F007C117D /* Supporting Files */, 94 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 95 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 96 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 97 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 98 | ); 99 | path = Runner; 100 | sourceTree = ""; 101 | }; 102 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | ); 106 | name = "Supporting Files"; 107 | sourceTree = ""; 108 | }; 109 | /* End PBXGroup section */ 110 | 111 | /* Begin PBXNativeTarget section */ 112 | 97C146ED1CF9000F007C117D /* Runner */ = { 113 | isa = PBXNativeTarget; 114 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 115 | buildPhases = ( 116 | 9740EEB61CF901F6004384FC /* Run Script */, 117 | 97C146EA1CF9000F007C117D /* Sources */, 118 | 97C146EB1CF9000F007C117D /* Frameworks */, 119 | 97C146EC1CF9000F007C117D /* Resources */, 120 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 121 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 122 | ); 123 | buildRules = ( 124 | ); 125 | dependencies = ( 126 | ); 127 | name = Runner; 128 | productName = Runner; 129 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 130 | productType = "com.apple.product-type.application"; 131 | }; 132 | /* End PBXNativeTarget section */ 133 | 134 | /* Begin PBXProject section */ 135 | 97C146E61CF9000F007C117D /* Project object */ = { 136 | isa = PBXProject; 137 | attributes = { 138 | LastUpgradeCheck = 1020; 139 | ORGANIZATIONNAME = "The Chromium Authors"; 140 | TargetAttributes = { 141 | 97C146ED1CF9000F007C117D = { 142 | CreatedOnToolsVersion = 7.3.1; 143 | LastSwiftMigration = 1100; 144 | }; 145 | }; 146 | }; 147 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 148 | compatibilityVersion = "Xcode 3.2"; 149 | developmentRegion = en; 150 | hasScannedForEncodings = 0; 151 | knownRegions = ( 152 | en, 153 | Base, 154 | ); 155 | mainGroup = 97C146E51CF9000F007C117D; 156 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 157 | projectDirPath = ""; 158 | projectRoot = ""; 159 | targets = ( 160 | 97C146ED1CF9000F007C117D /* Runner */, 161 | ); 162 | }; 163 | /* End PBXProject section */ 164 | 165 | /* Begin PBXResourcesBuildPhase section */ 166 | 97C146EC1CF9000F007C117D /* Resources */ = { 167 | isa = PBXResourcesBuildPhase; 168 | buildActionMask = 2147483647; 169 | files = ( 170 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 171 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 172 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 173 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXResourcesBuildPhase section */ 178 | 179 | /* Begin PBXShellScriptBuildPhase section */ 180 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 181 | isa = PBXShellScriptBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | ); 185 | inputPaths = ( 186 | ); 187 | name = "Thin Binary"; 188 | outputPaths = ( 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | shellPath = /bin/sh; 192 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 193 | }; 194 | 9740EEB61CF901F6004384FC /* Run Script */ = { 195 | isa = PBXShellScriptBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | ); 199 | inputPaths = ( 200 | ); 201 | name = "Run Script"; 202 | outputPaths = ( 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | shellPath = /bin/sh; 206 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 207 | }; 208 | /* End PBXShellScriptBuildPhase section */ 209 | 210 | /* Begin PBXSourcesBuildPhase section */ 211 | 97C146EA1CF9000F007C117D /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 216 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | /* End PBXSourcesBuildPhase section */ 221 | 222 | /* Begin PBXVariantGroup section */ 223 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 97C146FB1CF9000F007C117D /* Base */, 227 | ); 228 | name = Main.storyboard; 229 | sourceTree = ""; 230 | }; 231 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 232 | isa = PBXVariantGroup; 233 | children = ( 234 | 97C147001CF9000F007C117D /* Base */, 235 | ); 236 | name = LaunchScreen.storyboard; 237 | sourceTree = ""; 238 | }; 239 | /* End PBXVariantGroup section */ 240 | 241 | /* Begin XCBuildConfiguration section */ 242 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 243 | isa = XCBuildConfiguration; 244 | buildSettings = { 245 | ALWAYS_SEARCH_USER_PATHS = NO; 246 | CLANG_ANALYZER_NONNULL = YES; 247 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 248 | CLANG_CXX_LIBRARY = "libc++"; 249 | CLANG_ENABLE_MODULES = YES; 250 | CLANG_ENABLE_OBJC_ARC = YES; 251 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 252 | CLANG_WARN_BOOL_CONVERSION = YES; 253 | CLANG_WARN_COMMA = YES; 254 | CLANG_WARN_CONSTANT_CONVERSION = YES; 255 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 256 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 257 | CLANG_WARN_EMPTY_BODY = YES; 258 | CLANG_WARN_ENUM_CONVERSION = YES; 259 | CLANG_WARN_INFINITE_RECURSION = YES; 260 | CLANG_WARN_INT_CONVERSION = YES; 261 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 263 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 264 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 265 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 266 | CLANG_WARN_STRICT_PROTOTYPES = YES; 267 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 268 | CLANG_WARN_UNREACHABLE_CODE = YES; 269 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 270 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 271 | COPY_PHASE_STRIP = NO; 272 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 273 | ENABLE_NS_ASSERTIONS = NO; 274 | ENABLE_STRICT_OBJC_MSGSEND = YES; 275 | GCC_C_LANGUAGE_STANDARD = gnu99; 276 | GCC_NO_COMMON_BLOCKS = YES; 277 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 278 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 279 | GCC_WARN_UNDECLARED_SELECTOR = YES; 280 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 281 | GCC_WARN_UNUSED_FUNCTION = YES; 282 | GCC_WARN_UNUSED_VARIABLE = YES; 283 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 284 | MTL_ENABLE_DEBUG_INFO = NO; 285 | SDKROOT = iphoneos; 286 | SUPPORTED_PLATFORMS = iphoneos; 287 | TARGETED_DEVICE_FAMILY = "1,2"; 288 | VALIDATE_PRODUCT = YES; 289 | }; 290 | name = Profile; 291 | }; 292 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 293 | isa = XCBuildConfiguration; 294 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 295 | buildSettings = { 296 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 297 | CLANG_ENABLE_MODULES = YES; 298 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 299 | ENABLE_BITCODE = NO; 300 | FRAMEWORK_SEARCH_PATHS = ( 301 | "$(inherited)", 302 | "$(PROJECT_DIR)/Flutter", 303 | ); 304 | INFOPLIST_FILE = Runner/Info.plist; 305 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 306 | LIBRARY_SEARCH_PATHS = ( 307 | "$(inherited)", 308 | "$(PROJECT_DIR)/Flutter", 309 | ); 310 | PRODUCT_BUNDLE_IDENTIFIER = dev.basecontrol.signatureExample; 311 | PRODUCT_NAME = "$(TARGET_NAME)"; 312 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 313 | SWIFT_VERSION = 5.0; 314 | VERSIONING_SYSTEM = "apple-generic"; 315 | }; 316 | name = Profile; 317 | }; 318 | 97C147031CF9000F007C117D /* Debug */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ALWAYS_SEARCH_USER_PATHS = NO; 322 | CLANG_ANALYZER_NONNULL = YES; 323 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 324 | CLANG_CXX_LIBRARY = "libc++"; 325 | CLANG_ENABLE_MODULES = YES; 326 | CLANG_ENABLE_OBJC_ARC = YES; 327 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 328 | CLANG_WARN_BOOL_CONVERSION = YES; 329 | CLANG_WARN_COMMA = YES; 330 | CLANG_WARN_CONSTANT_CONVERSION = YES; 331 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 332 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 333 | CLANG_WARN_EMPTY_BODY = YES; 334 | CLANG_WARN_ENUM_CONVERSION = YES; 335 | CLANG_WARN_INFINITE_RECURSION = YES; 336 | CLANG_WARN_INT_CONVERSION = YES; 337 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 338 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 339 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 340 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 341 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 342 | CLANG_WARN_STRICT_PROTOTYPES = YES; 343 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 344 | CLANG_WARN_UNREACHABLE_CODE = YES; 345 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 346 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 347 | COPY_PHASE_STRIP = NO; 348 | DEBUG_INFORMATION_FORMAT = dwarf; 349 | ENABLE_STRICT_OBJC_MSGSEND = YES; 350 | ENABLE_TESTABILITY = YES; 351 | GCC_C_LANGUAGE_STANDARD = gnu99; 352 | GCC_DYNAMIC_NO_PIC = NO; 353 | GCC_NO_COMMON_BLOCKS = YES; 354 | GCC_OPTIMIZATION_LEVEL = 0; 355 | GCC_PREPROCESSOR_DEFINITIONS = ( 356 | "DEBUG=1", 357 | "$(inherited)", 358 | ); 359 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 360 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 361 | GCC_WARN_UNDECLARED_SELECTOR = YES; 362 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 363 | GCC_WARN_UNUSED_FUNCTION = YES; 364 | GCC_WARN_UNUSED_VARIABLE = YES; 365 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 366 | MTL_ENABLE_DEBUG_INFO = YES; 367 | ONLY_ACTIVE_ARCH = YES; 368 | SDKROOT = iphoneos; 369 | TARGETED_DEVICE_FAMILY = "1,2"; 370 | }; 371 | name = Debug; 372 | }; 373 | 97C147041CF9000F007C117D /* Release */ = { 374 | isa = XCBuildConfiguration; 375 | buildSettings = { 376 | ALWAYS_SEARCH_USER_PATHS = NO; 377 | CLANG_ANALYZER_NONNULL = YES; 378 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 379 | CLANG_CXX_LIBRARY = "libc++"; 380 | CLANG_ENABLE_MODULES = YES; 381 | CLANG_ENABLE_OBJC_ARC = YES; 382 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 383 | CLANG_WARN_BOOL_CONVERSION = YES; 384 | CLANG_WARN_COMMA = YES; 385 | CLANG_WARN_CONSTANT_CONVERSION = YES; 386 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 387 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 388 | CLANG_WARN_EMPTY_BODY = YES; 389 | CLANG_WARN_ENUM_CONVERSION = YES; 390 | CLANG_WARN_INFINITE_RECURSION = YES; 391 | CLANG_WARN_INT_CONVERSION = YES; 392 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 393 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 394 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 395 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 396 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 397 | CLANG_WARN_STRICT_PROTOTYPES = YES; 398 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 399 | CLANG_WARN_UNREACHABLE_CODE = YES; 400 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 401 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 402 | COPY_PHASE_STRIP = NO; 403 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 404 | ENABLE_NS_ASSERTIONS = NO; 405 | ENABLE_STRICT_OBJC_MSGSEND = YES; 406 | GCC_C_LANGUAGE_STANDARD = gnu99; 407 | GCC_NO_COMMON_BLOCKS = YES; 408 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 409 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 410 | GCC_WARN_UNDECLARED_SELECTOR = YES; 411 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 412 | GCC_WARN_UNUSED_FUNCTION = YES; 413 | GCC_WARN_UNUSED_VARIABLE = YES; 414 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 415 | MTL_ENABLE_DEBUG_INFO = NO; 416 | SDKROOT = iphoneos; 417 | SUPPORTED_PLATFORMS = iphoneos; 418 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 419 | TARGETED_DEVICE_FAMILY = "1,2"; 420 | VALIDATE_PRODUCT = YES; 421 | }; 422 | name = Release; 423 | }; 424 | 97C147061CF9000F007C117D /* Debug */ = { 425 | isa = XCBuildConfiguration; 426 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 427 | buildSettings = { 428 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 429 | CLANG_ENABLE_MODULES = YES; 430 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 431 | ENABLE_BITCODE = NO; 432 | FRAMEWORK_SEARCH_PATHS = ( 433 | "$(inherited)", 434 | "$(PROJECT_DIR)/Flutter", 435 | ); 436 | INFOPLIST_FILE = Runner/Info.plist; 437 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 438 | LIBRARY_SEARCH_PATHS = ( 439 | "$(inherited)", 440 | "$(PROJECT_DIR)/Flutter", 441 | ); 442 | PRODUCT_BUNDLE_IDENTIFIER = dev.basecontrol.signatureExample; 443 | PRODUCT_NAME = "$(TARGET_NAME)"; 444 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 445 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 446 | SWIFT_VERSION = 5.0; 447 | VERSIONING_SYSTEM = "apple-generic"; 448 | }; 449 | name = Debug; 450 | }; 451 | 97C147071CF9000F007C117D /* Release */ = { 452 | isa = XCBuildConfiguration; 453 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 454 | buildSettings = { 455 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 456 | CLANG_ENABLE_MODULES = YES; 457 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 458 | ENABLE_BITCODE = NO; 459 | FRAMEWORK_SEARCH_PATHS = ( 460 | "$(inherited)", 461 | "$(PROJECT_DIR)/Flutter", 462 | ); 463 | INFOPLIST_FILE = Runner/Info.plist; 464 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 465 | LIBRARY_SEARCH_PATHS = ( 466 | "$(inherited)", 467 | "$(PROJECT_DIR)/Flutter", 468 | ); 469 | PRODUCT_BUNDLE_IDENTIFIER = dev.basecontrol.signatureExample; 470 | PRODUCT_NAME = "$(TARGET_NAME)"; 471 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 472 | SWIFT_VERSION = 5.0; 473 | VERSIONING_SYSTEM = "apple-generic"; 474 | }; 475 | name = Release; 476 | }; 477 | /* End XCBuildConfiguration section */ 478 | 479 | /* Begin XCConfigurationList section */ 480 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 481 | isa = XCConfigurationList; 482 | buildConfigurations = ( 483 | 97C147031CF9000F007C117D /* Debug */, 484 | 97C147041CF9000F007C117D /* Release */, 485 | 249021D3217E4FDB00AE95B9 /* Profile */, 486 | ); 487 | defaultConfigurationIsVisible = 0; 488 | defaultConfigurationName = Release; 489 | }; 490 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 491 | isa = XCConfigurationList; 492 | buildConfigurations = ( 493 | 97C147061CF9000F007C117D /* Debug */, 494 | 97C147071CF9000F007C117D /* Release */, 495 | 249021D4217E4FDB00AE95B9 /* Profile */, 496 | ); 497 | defaultConfigurationIsVisible = 0; 498 | defaultConfigurationName = Release; 499 | }; 500 | /* End XCConfigurationList section */ 501 | }; 502 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 503 | } 504 | -------------------------------------------------------------------------------- /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 | 8 | -------------------------------------------------------------------------------- /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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/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 | signature_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" -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_svg/flutter_svg.dart'; 7 | import 'package:hand_signature/signature.dart'; 8 | 9 | import 'scroll_test.dart'; 10 | 11 | void main() => runApp(MyApp()); 12 | 13 | HandSignatureControl control = new HandSignatureControl( 14 | threshold: 0.01, 15 | smoothRatio: 0.65, 16 | velocityRange: 2.0, 17 | ); 18 | 19 | ValueNotifier svg = ValueNotifier(null); 20 | 21 | ValueNotifier rawImage = ValueNotifier(null); 22 | 23 | ValueNotifier rawImageFit = ValueNotifier(null); 24 | 25 | class MyApp extends StatelessWidget { 26 | bool get scrollTest => false; 27 | 28 | // This widget is the root of your application. 29 | @override 30 | Widget build(BuildContext context) { 31 | return MaterialApp( 32 | title: 'Signature Demo', 33 | theme: ThemeData( 34 | primarySwatch: Colors.blue, 35 | ), 36 | home: Scaffold( 37 | backgroundColor: Colors.orange, 38 | body: scrollTest 39 | ? ScrollTest() 40 | : SafeArea( 41 | child: Stack( 42 | children: [ 43 | Column( 44 | children: [ 45 | Expanded( 46 | child: Center( 47 | child: AspectRatio( 48 | aspectRatio: 2.0, 49 | child: Stack( 50 | children: [ 51 | Container( 52 | constraints: BoxConstraints.expand(), 53 | color: Colors.white, 54 | child: HandSignature( 55 | control: control, 56 | type: SignatureDrawType.shape, 57 | // supportedDevices: { 58 | // PointerDeviceKind.stylus, 59 | // }, 60 | ), 61 | ), 62 | CustomPaint( 63 | painter: DebugSignaturePainterCP( 64 | control: control, 65 | cp: false, 66 | cpStart: false, 67 | cpEnd: false, 68 | ), 69 | ), 70 | ], 71 | ), 72 | ), 73 | ), 74 | ), 75 | Row( 76 | children: [ 77 | CupertinoButton( 78 | onPressed: () { 79 | control.clear(); 80 | svg.value = null; 81 | rawImage.value = null; 82 | rawImageFit.value = null; 83 | }, 84 | child: Text('clear'), 85 | ), 86 | CupertinoButton( 87 | onPressed: () async { 88 | svg.value = control.toSvg( 89 | color: Colors.blueGrey, 90 | type: SignatureDrawType.shape, 91 | fit: true, 92 | ); 93 | 94 | rawImage.value = await control.toImage( 95 | color: Colors.blueAccent, 96 | background: Colors.greenAccent, 97 | fit: false, 98 | ); 99 | 100 | rawImageFit.value = await control.toImage( 101 | color: Colors.black, 102 | //background: Colors.greenAccent, 103 | fit: true, 104 | ); 105 | }, 106 | child: Text('export'), 107 | ), 108 | ], 109 | ), 110 | SizedBox( 111 | height: 16.0, 112 | ), 113 | ], 114 | ), 115 | Align( 116 | alignment: Alignment.bottomRight, 117 | child: Column( 118 | mainAxisSize: MainAxisSize.min, 119 | children: [ 120 | _buildImageView(), 121 | _buildScaledImageView(), 122 | _buildSvgView(), 123 | ], 124 | ), 125 | ), 126 | ], 127 | ), 128 | ), 129 | ), 130 | ); 131 | } 132 | 133 | Widget _buildImageView() => Container( 134 | width: 192.0, 135 | height: 96.0, 136 | decoration: BoxDecoration( 137 | border: Border.all(), 138 | color: Colors.white30, 139 | ), 140 | child: ValueListenableBuilder( 141 | valueListenable: rawImage, 142 | builder: (context, data, child) { 143 | if (data == null) { 144 | return Container( 145 | color: Colors.red, 146 | child: Center( 147 | child: Text('not signed yet (png)\nscaleToFill: false'), 148 | ), 149 | ); 150 | } else { 151 | return Padding( 152 | padding: EdgeInsets.all(8.0), 153 | child: Image.memory(data.buffer.asUint8List()), 154 | ); 155 | } 156 | }, 157 | ), 158 | ); 159 | 160 | Widget _buildScaledImageView() => Container( 161 | width: 192.0, 162 | height: 96.0, 163 | decoration: BoxDecoration( 164 | border: Border.all(), 165 | color: Colors.white30, 166 | ), 167 | child: ValueListenableBuilder( 168 | valueListenable: rawImageFit, 169 | builder: (context, data, child) { 170 | if (data == null) { 171 | return Container( 172 | color: Colors.red, 173 | child: Center( 174 | child: Text('not signed yet (png)\nscaleToFill: true'), 175 | ), 176 | ); 177 | } else { 178 | return Container( 179 | padding: EdgeInsets.all(8.0), 180 | color: Colors.orange, 181 | child: Image.memory(data.buffer.asUint8List()), 182 | ); 183 | } 184 | }, 185 | ), 186 | ); 187 | 188 | Widget _buildSvgView() => Container( 189 | width: 192.0, 190 | height: 96.0, 191 | decoration: BoxDecoration( 192 | border: Border.all(), 193 | color: Colors.white30, 194 | ), 195 | child: ValueListenableBuilder( 196 | valueListenable: svg, 197 | builder: (context, data, child) { 198 | if (data == null) { 199 | return Container( 200 | color: Colors.red, 201 | child: Center( 202 | child: Text('not signed yet (svg)'), 203 | ), 204 | ); 205 | } 206 | 207 | return Padding( 208 | padding: EdgeInsets.all(8.0), 209 | child: SvgPicture.string( 210 | data, 211 | placeholderBuilder: (_) => Container( 212 | color: Colors.lightBlueAccent, 213 | child: Center( 214 | child: Text('parsing data(svg)'), 215 | ), 216 | ), 217 | ), 218 | ); 219 | }, 220 | ), 221 | ); 222 | } 223 | -------------------------------------------------------------------------------- /example/lib/scroll_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hand_signature/signature.dart'; 3 | 4 | class ScrollTest extends StatefulWidget { 5 | @override 6 | _ScrollTestState createState() => _ScrollTestState(); 7 | } 8 | 9 | class _ScrollTestState extends State { 10 | final control = HandSignatureControl( 11 | threshold: 5.0, 12 | smoothRatio: 0.65, 13 | velocityRange: 2.0, 14 | ); 15 | 16 | bool scrollEnabled = true; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | backgroundColor: Colors.orange, 22 | body: ListView( 23 | physics: scrollEnabled 24 | ? BouncingScrollPhysics() 25 | : NeverScrollableScrollPhysics(), 26 | children: [ 27 | Container( 28 | height: MediaQuery.of(context).size.height * 0.35, 29 | ), 30 | Container( 31 | constraints: BoxConstraints.expand(height: 320.0), 32 | color: Colors.white, 33 | child: HandSignature( 34 | control: control, 35 | type: SignatureDrawType.shape, 36 | onPointerDown: () { 37 | setState(() { 38 | scrollEnabled = false; 39 | }); 40 | }, 41 | onPointerUp: () { 42 | setState(() { 43 | scrollEnabled = true; 44 | }); 45 | }, 46 | ), 47 | ), 48 | Container( 49 | height: MediaQuery.of(context).size.height * 0.65, 50 | color: Colors.deepOrange, 51 | ), 52 | ], 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: signature_example 2 | publish_to: none 3 | description: A new Flutter application. 4 | 5 | # The following defines the version and build number for your application. 6 | # A version number is three numbers separated by dots, like 1.2.43 7 | # followed by an optional build number separated by a +. 8 | # Both the version and the builder number may be overridden in flutter 9 | # build by specifying --build-name and --build-number, respectively. 10 | # In Android, build-name is used as versionName while build-number used as versionCode. 11 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 12 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 13 | # Read more about iOS versioning at 14 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 15 | version: 1.0.0+1 16 | 17 | environment: 18 | sdk: ">=3.0.0 <4.0.0" 19 | 20 | dependencies: 21 | flutter: 22 | sdk: flutter 23 | 24 | hand_signature: 25 | path: ../ 26 | 27 | flutter_svg: ^2.0.0+1 28 | 29 | dev_dependencies: 30 | flutter_test: 31 | sdk: flutter 32 | 33 | 34 | # For information on the generic Dart part of this file, see the 35 | # following page: https://dart.dev/tools/pub/pubspec 36 | 37 | # The following section is specific to Flutter. 38 | flutter: 39 | 40 | # The following line ensures that the Material Icons font is 41 | # included with your application, so that you can use the icons in 42 | # the material Icons class. 43 | uses-material-design: true 44 | 45 | # To add assets to your application, add an assets section, like this: 46 | # assets: 47 | # - images/a_dot_burr.jpeg 48 | # - images/a_dot_ham.jpeg 49 | 50 | # An image asset can refer to one or more resolution-specific "variants", see 51 | # https://flutter.dev/assets-and-images/#resolution-aware. 52 | 53 | # For details regarding adding assets from package dependencies, see 54 | # https://flutter.dev/assets-and-images/#from-packages 55 | 56 | # To add custom fonts to your application, add a fonts section here, 57 | # in this "flutter" section. Each entry in this list should have a 58 | # "family" key with the font family name, and a "fonts" key with a 59 | # list giving the asset and other descriptors for the font. For 60 | # example: 61 | # fonts: 62 | # - family: Schyler 63 | # fonts: 64 | # - asset: fonts/Schyler-Regular.ttf 65 | # - asset: fonts/Schyler-Italic.ttf 66 | # style: italic 67 | # - family: Trajan Pro 68 | # fonts: 69 | # - asset: fonts/TrajanPro.ttf 70 | # - asset: fonts/TrajanPro_Bold.ttf 71 | # weight: 700 72 | # 73 | # For details regarding fonts from package dependencies, 74 | # see https://flutter.dev/custom-fonts/#from-packages 75 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RomanBase/hand_signature/32790cb21d42847dd6829f940e50b3cf6adb8d49/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | example 18 | 19 | 20 | 21 | 24 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lib/signature.dart: -------------------------------------------------------------------------------- 1 | export 'src/signature_control.dart'; 2 | export 'src/signature_paint.dart'; 3 | export 'src/signature_painter.dart'; 4 | export 'src/signature_view.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/signature_control.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | import 'dart:typed_data'; 3 | import 'dart:ui'; 4 | 5 | import 'package:flutter/material.dart'; 6 | 7 | import '../signature.dart'; 8 | import 'utils.dart'; 9 | 10 | /// Paint settings. 11 | class SignaturePaintParams { 12 | /// Color of line. 13 | final Color color; 14 | 15 | /// Minimal width of line. 16 | final double strokeWidth; 17 | 18 | /// Maximal width of line. 19 | final double maxStrokeWidth; 20 | 21 | /// Hex value of [color]. 22 | String get hexColor => color.hexValue; 23 | 24 | /// Opacity of [color]. 25 | String get opacity => '${color.opacity}}'; 26 | 27 | /// Paint settings of line. 28 | /// [color] - color of line. 29 | /// [strokeWidth] - minimal width of line. 30 | /// [maxStrokeWidth] - maximal width of line. 31 | const SignaturePaintParams({ 32 | this.color = Colors.black, 33 | this.strokeWidth = 1.0, 34 | this.maxStrokeWidth = 10.0, 35 | }); 36 | } 37 | 38 | /// Extended [Offset] point with [timestamp]. 39 | class OffsetPoint extends Offset { 40 | /// Timestamp of this point. Used to determine velocity to other points. 41 | final int timestamp; 42 | 43 | /// 2D point in canvas space. 44 | /// [timestamp] of this [Offset]. Used to determine velocity to other points. 45 | const OffsetPoint({ 46 | required double dx, 47 | required double dy, 48 | required this.timestamp, 49 | }) : super(dx, dy); 50 | 51 | factory OffsetPoint.from(Offset offset) => OffsetPoint( 52 | dx: offset.dx, 53 | dy: offset.dy, 54 | timestamp: DateTime.now().millisecondsSinceEpoch, 55 | ); 56 | 57 | factory OffsetPoint.fromMap(Map map) => OffsetPoint( 58 | dx: map['x'], 59 | dy: map['y'], 60 | timestamp: map['t'], 61 | ); 62 | 63 | Map toMap() => { 64 | 'x': dx, 65 | 'y': dy, 66 | 't': timestamp, 67 | }; 68 | 69 | /// Returns velocity between this and [other] - previous point. 70 | double velocityFrom(OffsetPoint other) => timestamp != other.timestamp 71 | ? this.distanceTo(other) / (timestamp - other.timestamp) 72 | : 0.0; 73 | 74 | @override 75 | OffsetPoint translate(double translateX, double translateY) { 76 | return OffsetPoint( 77 | dx: dx + translateX, 78 | dy: dy + translateY, 79 | timestamp: timestamp, 80 | ); 81 | } 82 | 83 | @override 84 | OffsetPoint scale(double scaleX, double scaleY) { 85 | return OffsetPoint( 86 | dx: dx * scaleX, 87 | dy: dy * scaleY, 88 | timestamp: timestamp, 89 | ); 90 | } 91 | 92 | @override 93 | bool operator ==(other) { 94 | return other is OffsetPoint && 95 | other.dx == dx && 96 | other.dy == dy && 97 | other.timestamp == timestamp; 98 | } 99 | 100 | @override 101 | int get hashCode => Object.hash(super.hashCode, timestamp); 102 | } 103 | 104 | /// Line between two points. Curve of this line is controlled with other two points. 105 | /// Check https://cubic-bezier.com/ for more info about Bezier Curve. 106 | class CubicLine extends Offset { 107 | /// Initial point of curve. 108 | final OffsetPoint start; 109 | 110 | /// Control of [start] point. 111 | final Offset cpStart; 112 | 113 | /// Control of [end] point 114 | final Offset cpEnd; 115 | 116 | /// End point of curve. 117 | final OffsetPoint end; 118 | 119 | late double _velocity; 120 | late double _distance; 121 | 122 | /// Cache of Up vector. 123 | Offset? _upStartVector; 124 | 125 | /// Up vector of [start] point. 126 | Offset get upStartVector => 127 | _upStartVector ?? 128 | (_upStartVector = start.directionTo(point(0.001)).rotate(-math.pi * 0.5)); 129 | 130 | /// Cache of Up vector. 131 | Offset? _upEndVector; 132 | 133 | /// Up vector of [end] point. 134 | Offset get upEndVector => 135 | _upEndVector ?? 136 | (_upEndVector = end.directionTo(point(0.999)).rotate(math.pi * 0.5)); 137 | 138 | /// Down vector. 139 | Offset get _downStartVector => upStartVector.rotate(math.pi); 140 | 141 | /// Down vector. 142 | Offset get _downEndVector => upEndVector.rotate(math.pi); 143 | 144 | /// Start ratio size of line. 145 | double startSize; 146 | 147 | /// End ratio size of line. 148 | double endSize; 149 | 150 | /// Checks if point is dot. 151 | /// Returns 'true' if [start] and [end] is same -> [velocity] is zero. 152 | bool get isDot => _velocity == 0.0; 153 | 154 | /// Based on Bezier Cubic curve. 155 | /// [start] point of curve. 156 | /// [end] point of curve. 157 | /// [cpStart] - control point of [start] vector. 158 | /// [cpEnd] - control point of [end] vector. 159 | /// [startSize] - size ratio at begin of curve. 160 | /// [endSize] - size ratio at end of curve. 161 | /// [upStartVector] - pre-calculated Up vector fo start point. 162 | /// [upEndVector] - pre-calculated Up vector of end point. 163 | CubicLine({ 164 | required this.start, 165 | required this.cpStart, 166 | required this.cpEnd, 167 | required this.end, 168 | Offset? upStartVector, 169 | Offset? upEndVector, 170 | this.startSize = 0.0, 171 | this.endSize = 0.0, 172 | }) : super(start.dx, start.dy) { 173 | _upStartVector = upStartVector; 174 | _upEndVector = upEndVector; 175 | _velocity = end.velocityFrom(start); 176 | _distance = start.distanceTo(end); 177 | } 178 | 179 | @override 180 | CubicLine scale(double scaleX, double scaleY) => CubicLine( 181 | start: start.scale(scaleX, scaleY), 182 | cpStart: cpStart.scale(scaleX, scaleY), 183 | cpEnd: cpEnd.scale(scaleX, scaleY), 184 | end: end.scale(scaleX, scaleY), 185 | upStartVector: _upStartVector, 186 | upEndVector: _upEndVector, 187 | startSize: startSize * (scaleX + scaleY) * 0.5, 188 | endSize: endSize * (scaleX + scaleY) * 0.5, 189 | ); 190 | 191 | @override 192 | CubicLine translate(double translateX, double translateY) => CubicLine( 193 | start: start.translate(translateX, translateY), 194 | cpStart: cpStart.translate(translateX, translateY), 195 | cpEnd: cpEnd.translate(translateX, translateY), 196 | end: end.translate(translateX, translateY), 197 | upStartVector: _upStartVector, 198 | upEndVector: _upEndVector, 199 | startSize: startSize, 200 | endSize: endSize, 201 | ); 202 | 203 | /// Calculates length of Cubic curve with given [accuracy]. 204 | /// 0 - fastest, raw accuracy. 205 | /// 1 - slowest, most accurate. 206 | /// Returns length of curve. 207 | double length({double accuracy = 0.1}) { 208 | final steps = (accuracy * 100).toInt(); 209 | 210 | if (steps <= 1) { 211 | return _distance; 212 | } 213 | 214 | double length = 0.0; 215 | 216 | Offset prevPoint = start; 217 | for (int i = 1; i < steps; i++) { 218 | final t = i / steps; 219 | 220 | final next = point(t); 221 | 222 | length += prevPoint.distanceTo(next); 223 | prevPoint = next; 224 | } 225 | 226 | return length; 227 | } 228 | 229 | /// Calculates point on curve at given [t]. 230 | /// [t] - 0 to 1. 231 | /// Returns location on Curve at [t]. 232 | Offset point(double t) { 233 | final rt = 1.0 - t; 234 | return (start * rt * rt * rt) + 235 | (cpStart * 3.0 * rt * rt * t) + 236 | (cpEnd * 3.0 * rt * t * t) + 237 | (end * t * t * t); 238 | } 239 | 240 | /// Velocity along this line. 241 | double velocity({double accuracy = 0.0}) => start.timestamp != end.timestamp 242 | ? length(accuracy: accuracy) / (end.timestamp - start.timestamp) 243 | : 0.0; 244 | 245 | /// Combines line velocity with [inVelocity] based on [velocityRatio]. 246 | double combineVelocity(double inVelocity, 247 | {double velocityRatio = 0.65, double maxFallOff = 1.0}) { 248 | final value = 249 | (_velocity * velocityRatio) + (inVelocity * (1.0 - velocityRatio)); 250 | 251 | maxFallOff *= _distance / 10.0; 252 | 253 | final dif = value - inVelocity; 254 | if (dif.abs() > maxFallOff) { 255 | if (dif > 0.0) { 256 | return inVelocity + maxFallOff; 257 | } else { 258 | return inVelocity - maxFallOff; 259 | } 260 | } 261 | 262 | return value; 263 | } 264 | 265 | /// Converts this line to Cubic [Path]. 266 | Path toPath() => Path() 267 | ..moveTo(dx, dy) 268 | ..cubicTo(cpStart.dx, cpStart.dy, cpEnd.dx, cpEnd.dy, end.dx, end.dy); 269 | 270 | /// Converts this line to [CubicArc]. 271 | List toArc(double size, double deltaSize, 272 | {double precision = 0.5}) { 273 | final list = []; 274 | 275 | final steps = (_distance * precision).floor().clamp(1, 30); 276 | 277 | Offset start = this.start; 278 | for (int i = 0; i < steps; i++) { 279 | final t = (i + 1) / steps; 280 | final loc = point(t); 281 | final width = size + deltaSize * t; 282 | 283 | list.add(CubicArc( 284 | start: start, 285 | location: loc, 286 | size: width, 287 | )); 288 | 289 | start = loc; 290 | } 291 | 292 | return list; 293 | } 294 | 295 | /// Converts this line to closed [Path]. 296 | Path toShape(double size, double maxSize) { 297 | final startArm = (size + (maxSize - size) * startSize) * 0.5; 298 | final endArm = (size + (maxSize - size) * endSize) * 0.5; 299 | 300 | final sDirUp = upStartVector; 301 | final eDirUp = upEndVector; 302 | 303 | final d1 = sDirUp * startArm; 304 | final d2 = eDirUp * endArm; 305 | final d3 = eDirUp.rotate(math.pi) * endArm; 306 | final d4 = sDirUp.rotate(math.pi) * startArm; 307 | 308 | return Path() 309 | ..start(start + d1) 310 | ..cubic(cpStart + d1, cpEnd + d2, end + d2) 311 | ..line(end + d3) 312 | ..cubic(cpEnd + d3, cpStart + d4, start + d4) 313 | ..close(); 314 | } 315 | 316 | /// Returns Up offset of start point. 317 | Offset cpsUp(double size, double maxSize) => 318 | upStartVector * startRadius(size, maxSize); 319 | 320 | /// Returns Up offset of end point. 321 | Offset cpeUp(double size, double maxSize) => 322 | upEndVector * endRadius(size, maxSize); 323 | 324 | /// Returns Down offset of start point. 325 | Offset cpsDown(double size, double maxSize) => 326 | _downStartVector * startRadius(size, maxSize); 327 | 328 | /// Returns Down offset of end point. 329 | Offset cpeDown(double size, double maxSize) => 330 | _downEndVector * endRadius(size, maxSize); 331 | 332 | /// Returns radius of start point. 333 | double startRadius(double size, double maxSize) => 334 | _lerpRadius(size, maxSize, startSize); 335 | 336 | /// Returns radius of end point. 337 | double endRadius(double size, double maxSize) => 338 | _lerpRadius(size, maxSize, endSize); 339 | 340 | /// Linear interpolation of size. 341 | /// Returns radius of interpolated size. 342 | double _lerpRadius(double size, double maxSize, double t) => 343 | (size + (maxSize - size) * t) * 0.5; 344 | 345 | /// Calculates [current] point based on [previous] and [next] control points. 346 | static Offset softCP(OffsetPoint current, 347 | {OffsetPoint? previous, 348 | OffsetPoint? next, 349 | bool reverse = false, 350 | double smoothing = 0.65}) { 351 | assert(smoothing >= 0.0 && smoothing <= 1.0); 352 | 353 | previous ??= current; 354 | next ??= current; 355 | 356 | final sharpness = 1.0 - smoothing; 357 | 358 | final dist1 = previous.distanceTo(current); 359 | final dist2 = current.distanceTo(next); 360 | final dist = dist1 + dist2; 361 | final dir1 = current.directionTo(next); 362 | final dir2 = current.directionTo(previous); 363 | final dir3 = 364 | reverse ? next.directionTo(previous) : previous.directionTo(next); 365 | 366 | final velocity = 367 | (dist * 0.3 / (next.timestamp - previous.timestamp)).clamp(0.5, 3.0); 368 | final ratio = (dist * velocity * smoothing) 369 | .clamp(0.0, (reverse ? dist2 : dist1) * 0.5); 370 | 371 | final dir = 372 | ((reverse ? dir2 : dir1) * sharpness) + (dir3 * smoothing) * ratio; 373 | final x = current.dx + dir.dx; 374 | final y = current.dy + dir.dy; 375 | 376 | return Offset(x, y); 377 | } 378 | 379 | @override 380 | bool operator ==(Object other) => 381 | other is CubicLine && 382 | start == other.start && 383 | cpStart == other.cpStart && 384 | cpEnd == other.cpEnd && 385 | end == other.end && 386 | startSize == other.startSize && 387 | endSize == other.endSize; 388 | 389 | @override 390 | int get hashCode => 391 | super.hashCode ^ 392 | start.hashCode ^ 393 | cpStart.hashCode ^ 394 | cpEnd.hashCode ^ 395 | end.hashCode ^ 396 | startSize.hashCode ^ 397 | endSize.hashCode; 398 | } 399 | 400 | /// Arc between two points. 401 | class CubicArc extends Offset { 402 | static const _pi2 = math.pi * 2.0; 403 | 404 | /// End location of arc. 405 | final Offset location; 406 | 407 | /// Line size. 408 | final double size; 409 | 410 | /// Arc path. 411 | Path get path => Path() 412 | ..moveTo(dx, dy) 413 | ..arcToPoint(location, rotation: _pi2); 414 | 415 | /// Rectangle of start and end point. 416 | Rect get rect => Rect.fromPoints(this, location); 417 | 418 | /// Arc line. 419 | /// [start] point of arc. 420 | /// [location] end point of arc. 421 | /// [size] ratio of arc. typically 0 - 1. 422 | CubicArc({ 423 | required Offset start, 424 | required this.location, 425 | this.size = 1.0, 426 | }) : super(start.dx, start.dy); 427 | 428 | @override 429 | Offset translate(double translateX, double translateY) => CubicArc( 430 | start: Offset(dx + translateX, dy + translateY), 431 | location: location.translate(translateX, translateY), 432 | size: size, 433 | ); 434 | 435 | @override 436 | Offset scale(double scaleX, double scaleY) => CubicArc( 437 | start: Offset(dx * scaleX, dy * scaleY), 438 | location: location.scale(scaleX, scaleY), 439 | size: size * (scaleX + scaleY) * 0.5, 440 | ); 441 | } 442 | 443 | /// Combines sequence of points into one Line. 444 | class CubicPath { 445 | /// Raw data. 446 | final _points = []; 447 | 448 | /// [CubicLine] representation of path. 449 | final _lines = []; 450 | 451 | /// [CubicArc] representation of path. 452 | final _arcs = []; 453 | 454 | /// Distance between two control points. 455 | final double threshold; 456 | 457 | /// Ratio of line smoothing. 458 | /// Don't have impact to performance. Values between 0 - 1. 459 | /// [0] - no smoothing, no flattening. 460 | /// [1] - best smoothing, but flattened. 461 | /// Best results are between: 0.5 - 0.85. 462 | final double smoothRatio; 463 | 464 | /// Returns raw data of path. 465 | List get points => _points; 466 | 467 | /// Returns [CubicLine] representation of path. 468 | List get lines => _lines; 469 | 470 | /// Returns [CubicArc] representation of path. 471 | List get arcs => _arcs; 472 | 473 | /// First point of path. 474 | Offset? get _origin => _points.isNotEmpty ? _points[0] : null; 475 | 476 | /// Last point of path. 477 | OffsetPoint? get _lastPoint => 478 | _points.isNotEmpty ? _points[_points.length - 1] : null; 479 | 480 | /// Checks if path is valid. 481 | bool get isFilled => _lines.isNotEmpty; 482 | 483 | /// Checks if this Line is just dot. 484 | bool get isDot => lines.length == 1 && lines[0].isDot; 485 | 486 | /// Unfinished path. 487 | // Path? _temp; 488 | 489 | /// Returns currently unfinished part of path. 490 | // Path? get tempPath => _temp; 491 | 492 | /// Maximum possible velocity. 493 | double _maxVelocity = 1.0; 494 | 495 | /// Actual average velocity. 496 | double _currentVelocity = 0.0; 497 | 498 | /// Actual size based on velocity. 499 | double _currentSize = 0.0; 500 | 501 | /// Line builder. 502 | /// [threshold] - Distance between two control points. 503 | /// [smoothRatio] - Ratio of line smoothing. 504 | CubicPath({ 505 | this.threshold = 3.0, 506 | this.smoothRatio = 0.65, 507 | }); 508 | 509 | /// Adds line to path. 510 | void _addLine(CubicLine line) { 511 | if (_lines.length == 0) { 512 | if (_currentVelocity == 0.0) { 513 | _currentVelocity = line._velocity; 514 | } 515 | 516 | if (_currentSize == 0.0) { 517 | _currentSize = _lineSize(_currentVelocity, _maxVelocity); 518 | } 519 | } else { 520 | line._upStartVector = _lines.last.upEndVector; 521 | } 522 | 523 | _lines.add(line); 524 | 525 | final combinedVelocity = 526 | line.combineVelocity(_currentVelocity, maxFallOff: 0.125); 527 | final double endSize = _lineSize(combinedVelocity, _maxVelocity); 528 | 529 | if (combinedVelocity > _maxVelocity) { 530 | _maxVelocity = combinedVelocity; 531 | } 532 | 533 | line.startSize = _currentSize; 534 | line.endSize = endSize; 535 | 536 | _arcs.addAll(line.toArc(_currentSize, endSize - _currentSize)); 537 | 538 | _currentSize = endSize; 539 | _currentVelocity = combinedVelocity; 540 | } 541 | 542 | /// Adds dot to path. 543 | void _addDot(CubicLine line) { 544 | final size = 0.25 + _lineSize(_currentVelocity, _maxVelocity) * 0.5; 545 | line.startSize = size; 546 | 547 | _lines.add(line); 548 | _arcs.addAll(line.toArc(size, 0.0)); 549 | } 550 | 551 | /// Calculates line size based on [velocity]. 552 | double _lineSize(double velocity, double max) { 553 | velocity /= max; 554 | 555 | return 1.0 - velocity.clamp(0.0, 1.0); 556 | } 557 | 558 | /// Starts path at given [point]. 559 | /// Must be called as first, before [begin], [end]. 560 | void begin(Offset point, {double velocity = 0.0}) { 561 | _points.add(point is OffsetPoint ? point : OffsetPoint.from(point)); 562 | _currentVelocity = velocity; 563 | } 564 | 565 | /// Alters path with given [point]. 566 | void add(Offset point) { 567 | assert(_origin != null); 568 | 569 | final nextPoint = point is OffsetPoint ? point : OffsetPoint.from(point); 570 | 571 | if (_lastPoint == null || _lastPoint!.distanceTo(nextPoint) < threshold) { 572 | return; 573 | } 574 | 575 | _points.add(nextPoint); 576 | int count = _points.length; 577 | 578 | if (count < 3) { 579 | return; 580 | } 581 | 582 | int i = count - 3; 583 | 584 | final prev = i > 0 ? _points[i - 1] : _points[i]; 585 | final start = _points[i]; 586 | final end = _points[i + 1]; 587 | final next = _points[i + 2]; 588 | 589 | final cpStart = CubicLine.softCP( 590 | start, 591 | previous: prev, 592 | next: end, 593 | smoothing: smoothRatio, 594 | ); 595 | 596 | final cpEnd = CubicLine.softCP( 597 | end, 598 | previous: start, 599 | next: next, 600 | smoothing: smoothRatio, 601 | reverse: true, 602 | ); 603 | 604 | final line = CubicLine( 605 | start: start, 606 | cpStart: cpStart, 607 | cpEnd: cpEnd, 608 | end: end, 609 | ); 610 | 611 | _addLine(line); 612 | } 613 | 614 | /// Ends path at given [point]. 615 | bool end({Offset? point}) { 616 | if (point != null) { 617 | add(point); 618 | } 619 | 620 | if (_points.isEmpty) { 621 | return false; 622 | } 623 | 624 | if (_points.length < 3) { 625 | if (_points.length == 1 || _points[0].distanceTo(points[1]) == 0.0) { 626 | _addDot(CubicLine( 627 | start: _points[0], 628 | cpStart: _points[0], 629 | cpEnd: _points[0], 630 | end: _points[0], 631 | )); 632 | } else { 633 | _addLine(CubicLine( 634 | start: _points[0], 635 | cpStart: _points[0], 636 | cpEnd: _points[1], 637 | end: _points[1], 638 | )); 639 | } 640 | } else { 641 | final i = _points.length - 3; 642 | 643 | if (_points[i + 1].distanceTo(points[i + 2]) > 0.0) { 644 | _addLine(CubicLine( 645 | start: _points[i + 1], 646 | cpStart: _points[i + 1], 647 | cpEnd: _points[i + 2], 648 | end: _points[i + 2], 649 | )); 650 | } 651 | } 652 | 653 | return true; 654 | } 655 | 656 | /// Sets scale of whole line. 657 | void setScale(double ratio) { 658 | if (!isFilled) { 659 | return; 660 | } 661 | 662 | final arcData = PathUtil.scale(_arcs, ratio); 663 | _arcs 664 | ..clear() 665 | ..addAll(arcData); 666 | 667 | final lineData = PathUtil.scale(_lines, ratio); 668 | _lines 669 | ..clear() 670 | ..addAll(lineData); 671 | } 672 | 673 | /// Clears all path data-. 674 | void clear() { 675 | _points.clear(); 676 | _lines.clear(); 677 | _arcs.clear(); 678 | } 679 | 680 | /// Currently checks only equality of [points]. 681 | bool equals(CubicPath other) { 682 | if (points.length == other.points.length) { 683 | for (int i = 0; i < points.length; i++) { 684 | if (points[i] != other.points[i]) { 685 | return false; 686 | } 687 | } 688 | 689 | return true; 690 | } 691 | 692 | return false; 693 | } 694 | } 695 | 696 | /// Controls signature drawing and line shape. 697 | /// Also handles export of finished signature. 698 | class HandSignatureControl extends ChangeNotifier { 699 | /// Distance between two control points. 700 | final double threshold; 701 | 702 | /// Smoothing ratio of path. 703 | final double smoothRatio; 704 | 705 | /// Maximal velocity. 706 | final double velocityRange; 707 | 708 | /// List of active paths. 709 | final _paths = []; 710 | 711 | /// List of currently completed lines. 712 | List get paths => _paths; 713 | 714 | /// Lazy list of all control points - raw data. 715 | List> get _offsets { 716 | final list = >[]; 717 | 718 | _paths.forEach((data) => list.add(data._points)); 719 | 720 | return list; 721 | } 722 | 723 | /// Lazy list of all Lines. 724 | List> get _cubicLines { 725 | final list = >[]; 726 | 727 | _paths.forEach((data) => list.add(data._lines)); 728 | 729 | return list; 730 | } 731 | 732 | /// Lazy list of all Arcs. 733 | List get _arcs { 734 | final list = []; 735 | 736 | _paths.forEach((data) => list.addAll(data.arcs)); 737 | 738 | return list; 739 | } 740 | 741 | /// Lazy list of all Lines. 742 | List get lines { 743 | final list = []; 744 | 745 | _paths.forEach((data) => list.addAll(data._lines)); 746 | 747 | return list; 748 | } 749 | 750 | /// Currently unfinished path. 751 | CubicPath? _activePath; 752 | 753 | /// Visual parameters of line painting. 754 | SignaturePaintParams? params; 755 | 756 | /// Canvas size. 757 | Size _areaSize = Size.zero; 758 | 759 | /// Checks if is there unfinished path. 760 | bool get hasActivePath => _activePath != null; 761 | 762 | /// Checks if something is drawn. 763 | bool get isFilled => _paths.isNotEmpty; 764 | 765 | /// Controls input from [HandSignature] and creates smooth signature path. 766 | /// [threshold] minimal distance between two points. 767 | /// [smoothRatio] smoothing ratio of curved parts. 768 | /// [velocityRange] controls velocity speed and dampening between points (only Shape and Arc drawing types using this property to control line width). aka how fast si signature drawn.. 769 | HandSignatureControl({ 770 | this.threshold = 3.0, 771 | this.smoothRatio = 0.65, 772 | this.velocityRange = 2.0, 773 | }) : assert(threshold > 0.0), 774 | assert(smoothRatio > 0.0 && smoothRatio <= 1.0), 775 | assert(velocityRange > 0.0); 776 | 777 | factory HandSignatureControl.fromMap(Map data) => 778 | HandSignatureControl( 779 | smoothRatio: data['smoothRatio'], 780 | threshold: data['threshold'], 781 | velocityRange: data['velocityRange'], 782 | )..importData(data); 783 | 784 | /// Starts new line at given [point]. 785 | void startPath(Offset point) { 786 | assert(!hasActivePath); 787 | 788 | _activePath = CubicPath( 789 | threshold: threshold, 790 | smoothRatio: smoothRatio, 791 | ).._maxVelocity = velocityRange; 792 | 793 | _activePath!.begin(point, 794 | velocity: _paths.isNotEmpty ? _paths.last._currentVelocity : 0.0); 795 | 796 | _paths.add(_activePath!); 797 | } 798 | 799 | /// Adds [point[ to active path. 800 | void alterPath(Offset point) { 801 | assert(hasActivePath); 802 | 803 | _activePath?.add(point); 804 | 805 | notifyListeners(); 806 | } 807 | 808 | /// Closes active path at given [point]. 809 | void closePath([Offset? point]) { 810 | assert(hasActivePath); 811 | 812 | final valid = _activePath?.end(point: point); 813 | 814 | if (valid == false) { 815 | _paths.removeLast(); 816 | } 817 | 818 | _activePath = null; 819 | 820 | notifyListeners(); 821 | } 822 | 823 | /// Imports given [paths] and alters current signature data. 824 | void importPath(List paths, [Size? bounds]) { 825 | //TODO: check bounds 826 | 827 | if (bounds != null) { 828 | if (_areaSize.isEmpty) { 829 | print( 830 | 'Signature: Canvas area is not specified yet. Signature can be out of visible bounds or misplaced.'); 831 | } else if (_areaSize != bounds) { 832 | print( 833 | 'Signature: Canvas area has different size. Signature can be out of visible bounds or misplaced.'); 834 | } 835 | } 836 | 837 | _paths.addAll(paths); 838 | notifyListeners(); 839 | } 840 | 841 | /// Expects [data] from [toMap]. 842 | void importData(Map data) { 843 | final list = []; 844 | 845 | final bounds = Size(data['bounds']['width'], data['bounds']['height']); 846 | final paths = data['paths']; 847 | final threshold = data['threshold']; 848 | final smoothRatio = data['smoothRatio']; 849 | final velocityRange = data['velocityRange']; 850 | 851 | for (final path in paths) { 852 | final List points = List.from(path); 853 | 854 | final cp = CubicPath( 855 | threshold: threshold, 856 | smoothRatio: smoothRatio, 857 | ).._maxVelocity = velocityRange; 858 | 859 | cp.begin(OffsetPoint.fromMap(points[0])); 860 | points.skip(1).forEach((element) => cp.add(OffsetPoint.fromMap(element))); 861 | cp.end(); 862 | 863 | list.add(cp); 864 | } 865 | 866 | importPath(list, bounds); 867 | } 868 | 869 | /// Removes last line. 870 | bool stepBack() { 871 | assert(!hasActivePath); 872 | 873 | if (_paths.isNotEmpty) { 874 | _paths.removeLast(); 875 | notifyListeners(); 876 | 877 | return true; 878 | } 879 | 880 | return false; 881 | } 882 | 883 | /// Clears all data. 884 | void clear() { 885 | _paths.clear(); 886 | 887 | notifyListeners(); 888 | } 889 | 890 | //TODO: Only landscape to landscape mode works correctly now. Add support for orientation switching. 891 | /// Handles canvas size changes. 892 | bool notifyDimension(Size size) { 893 | if (_areaSize == size) { 894 | return false; 895 | } 896 | 897 | if (_areaSize.isEmpty || 898 | _areaSize.width == size.width || 899 | _areaSize.height == size.height) { 900 | _areaSize = size; 901 | return false; 902 | } 903 | 904 | //TODO: iOS device holds pointer during rotation 905 | if (hasActivePath) { 906 | closePath(); 907 | } 908 | 909 | if (!isFilled) { 910 | _areaSize = size; 911 | return false; 912 | } 913 | 914 | //final ratioX = size.width / _areaSize.width; 915 | final ratioY = size.height / _areaSize.height; 916 | final scale = ratioY; 917 | 918 | _areaSize = size; 919 | 920 | _paths.forEach((path) { 921 | path.setScale(scale); 922 | }); 923 | 924 | //TODO: Called during rebuild, so notify must be postponed one frame - will be solved by widget/state 925 | Future.delayed(Duration(), () => notifyListeners()); 926 | 927 | return true; 928 | } 929 | 930 | /// Converts dat to Map (json) 931 | /// Exported data can be restored via [HandSignatureControl.fromMap] factory or via [importData] method. 932 | Map toMap() => { 933 | 'bounds': { 934 | 'width': _areaSize.width, 935 | 'height': _areaSize.height, 936 | }, 937 | 'paths': 938 | paths.map((p) => p.points.map((p) => p.toMap()).toList()).toList(), 939 | 'threshold': threshold, 940 | 'smoothRatio': smoothRatio, 941 | 'velocityRange': velocityRange, 942 | }; 943 | 944 | /// Converts data to [svg] String. 945 | /// [type] - data structure. 946 | String? toSvg({ 947 | SignatureDrawType type = SignatureDrawType.shape, 948 | int width = 512, 949 | int height = 256, 950 | double border = 0.0, 951 | Color? color, 952 | double? strokeWidth, 953 | double? maxStrokeWidth, 954 | bool fit = false, 955 | }) { 956 | if (!isFilled) { 957 | return null; 958 | } 959 | 960 | params ??= SignaturePaintParams( 961 | color: Colors.black, 962 | strokeWidth: 1.0, 963 | maxStrokeWidth: 10.0, 964 | ); 965 | 966 | color ??= params!.color; 967 | strokeWidth ??= params!.strokeWidth; 968 | maxStrokeWidth ??= params!.maxStrokeWidth; 969 | 970 | final bounds = PathUtil.boundsOf(_offsets, radius: maxStrokeWidth * 0.5); 971 | final fitBox = 972 | bounds.size.scaleToFit(Size(width.toDouble(), height.toDouble())); 973 | final rect = fit 974 | ? Rect.fromLTWH(0.0, 0.0, fitBox.width, fitBox.height) 975 | : Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()); 976 | 977 | if (type == SignatureDrawType.line || type == SignatureDrawType.shape) { 978 | final data = PathUtil.fillData( 979 | _cubicLines, 980 | rect, 981 | bound: bounds, 982 | border: maxStrokeWidth + border, 983 | ); 984 | 985 | if (type == SignatureDrawType.line) { 986 | return _exportPathSvg(data, rect.size, color, strokeWidth); 987 | } else { 988 | return _exportShapeSvg( 989 | data, rect.size, color, strokeWidth, maxStrokeWidth); 990 | } 991 | } else { 992 | final data = PathUtil.fill( 993 | _arcs, 994 | rect, 995 | bound: bounds, 996 | border: maxStrokeWidth + border, 997 | ); 998 | 999 | return _exportArcSvg(data, rect.size, color, strokeWidth, maxStrokeWidth); 1000 | } 1001 | } 1002 | 1003 | /// Exports [svg] as simple line. 1004 | String _exportPathSvg( 1005 | List> data, 1006 | Size size, 1007 | Color color, 1008 | double strokeWidth, 1009 | ) { 1010 | final buffer = StringBuffer(); 1011 | buffer.writeln(''); 1012 | buffer.writeln( 1013 | ''); 1014 | buffer.writeln( 1015 | ''); 1016 | 1017 | data.forEach((line) { 1018 | buffer.write(''); 1022 | }); 1023 | 1024 | buffer.writeln(''); 1025 | buffer.writeln(''); 1026 | 1027 | return buffer.toString(); 1028 | } 1029 | 1030 | /// Exports [svg] as a lot of arcs. 1031 | String _exportArcSvg( 1032 | List data, 1033 | Size size, 1034 | Color color, 1035 | double strokeWidth, 1036 | double maxStrokeWidth, 1037 | ) { 1038 | final buffer = StringBuffer(); 1039 | buffer.writeln(''); 1040 | buffer.writeln( 1041 | ''); 1042 | buffer.writeln( 1043 | ''); 1044 | 1045 | data.forEach((arc) { 1046 | final strokeSize = 1047 | strokeWidth + (maxStrokeWidth - strokeWidth) * arc.size; 1048 | buffer.writeln( 1049 | ''); 1050 | }); 1051 | 1052 | buffer.writeln(''); 1053 | buffer.writeln(''); 1054 | 1055 | return buffer.toString(); 1056 | } 1057 | 1058 | /// Exports [svg] as shape - 4 paths per line. Path is closed and filled with given color. 1059 | String _exportShapeSvg( 1060 | List> data, 1061 | Size size, 1062 | Color color, 1063 | double strokeWidth, 1064 | double maxStrokeWidth, 1065 | ) { 1066 | final buffer = StringBuffer(); 1067 | buffer.writeln(''); 1068 | buffer.writeln( 1069 | ''); 1070 | buffer.writeln(''); 1071 | 1072 | data.forEach((lines) { 1073 | if (lines.length == 1 && lines[0].isDot) { 1074 | final dot = lines[0]; 1075 | buffer.writeln( 1076 | ''); 1077 | } else { 1078 | final firstLine = lines.first; 1079 | final start = 1080 | firstLine.start + firstLine.cpsUp(strokeWidth, maxStrokeWidth); 1081 | buffer.write(''); 1115 | 1116 | buffer.writeln( 1117 | ''); 1118 | buffer.writeln( 1119 | ''); 1120 | } 1121 | }); 1122 | 1123 | buffer.writeln(''); 1124 | buffer.writeln(''); 1125 | 1126 | return buffer.toString(); 1127 | } 1128 | 1129 | /// Exports data to [Picture]. 1130 | /// 1131 | /// If [fit] is enabled, the path will be normalized and scaled to fit given [width] and [height]. 1132 | Picture? toPicture({ 1133 | int width = 512, 1134 | int height = 256, 1135 | Color? color, 1136 | Color? background, 1137 | double? strokeWidth, 1138 | double? maxStrokeWidth, 1139 | double border = 0.0, 1140 | bool fit = true, 1141 | }) { 1142 | if (!isFilled) { 1143 | return null; 1144 | } 1145 | 1146 | final pictureRect = Rect.fromLTRB( 1147 | 0.0, 1148 | 0.0, 1149 | width.toDouble(), 1150 | height.toDouble(), 1151 | ); 1152 | 1153 | params ??= SignaturePaintParams( 1154 | color: Colors.black, 1155 | strokeWidth: 1.0, 1156 | maxStrokeWidth: 10.0, 1157 | ); 1158 | 1159 | color ??= params!.color; 1160 | strokeWidth ??= params!.strokeWidth; 1161 | maxStrokeWidth ??= params!.maxStrokeWidth; 1162 | 1163 | final canvasRect = Rect.fromLTRB(0, 0, _areaSize.width, _areaSize.height); 1164 | final data = fit 1165 | ? PathUtil.fill( 1166 | _arcs, 1167 | pictureRect, 1168 | radius: maxStrokeWidth * 0.5, 1169 | border: border, 1170 | ) 1171 | : PathUtil.fill( 1172 | _arcs, 1173 | pictureRect, 1174 | bound: canvasRect, 1175 | border: border, 1176 | ); 1177 | final path = CubicPath().._arcs.addAll(data); 1178 | 1179 | final recorder = PictureRecorder(); 1180 | final painter = PathSignaturePainter( 1181 | paths: [path], 1182 | color: color, 1183 | width: strokeWidth, 1184 | maxWidth: maxStrokeWidth, 1185 | type: SignatureDrawType.arc, 1186 | ); 1187 | 1188 | final canvas = Canvas( 1189 | recorder, 1190 | Rect.fromPoints( 1191 | Offset(0.0, 0.0), 1192 | Offset(width.toDouble(), height.toDouble()), 1193 | ), 1194 | ); 1195 | 1196 | if (background != null) { 1197 | canvas.drawColor(background, BlendMode.src); 1198 | } 1199 | 1200 | painter.paint(canvas, Size(width.toDouble(), height.toDouble())); 1201 | 1202 | return recorder.endRecording(); 1203 | } 1204 | 1205 | /// Exports data to raw image. 1206 | /// 1207 | /// If [fit] is enabled, the path will be normalized and scaled to fit given [width] and [height]. 1208 | Future toImage({ 1209 | int width = 512, 1210 | int height = 256, 1211 | Color? color, 1212 | Color? background, 1213 | double? strokeWidth, 1214 | double? maxStrokeWidth, 1215 | double border = 32.0, 1216 | ImageByteFormat format = ImageByteFormat.png, 1217 | bool fit = false, 1218 | }) async { 1219 | final image = await toPicture( 1220 | width: width, 1221 | height: height, 1222 | color: color, 1223 | background: background, 1224 | strokeWidth: strokeWidth, 1225 | maxStrokeWidth: maxStrokeWidth, 1226 | border: border, 1227 | fit: fit, 1228 | )?.toImage(width, height); 1229 | 1230 | if (image == null) { 1231 | return null; 1232 | } 1233 | 1234 | return image.toByteData(format: format); 1235 | } 1236 | 1237 | /// Currently checks only equality of [paths]. 1238 | bool equals(HandSignatureControl other) { 1239 | if (paths.length == other.paths.length) { 1240 | for (int i = 0; i < paths.length; i++) { 1241 | if (!paths[i].equals(other.paths[i])) { 1242 | return false; 1243 | } 1244 | } 1245 | 1246 | return true; 1247 | } 1248 | 1249 | return false; 1250 | } 1251 | 1252 | @override 1253 | void dispose() { 1254 | _paths.clear(); 1255 | _activePath = null; 1256 | 1257 | super.dispose(); 1258 | } 1259 | } 1260 | -------------------------------------------------------------------------------- /lib/src/signature_paint.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../signature.dart'; 4 | 5 | /// Creates [CustomPaint] and rebuilds whenever signature data are changed. 6 | /// All arguments are passed to [PathSignaturePainter]. 7 | /// 8 | /// Check [HandSignature] and [HandSignatureView]. 9 | class HandSignaturePaint extends StatefulWidget { 10 | /// Paths controller. 11 | final HandSignatureControl control; 12 | 13 | /// Color of path. 14 | final Color color; 15 | 16 | /// Minimal size of path. 17 | final double strokeWidth; 18 | 19 | /// Maximal size of path. 20 | final double maxStrokeWidth; 21 | 22 | /// Path type. 23 | final SignatureDrawType type; 24 | 25 | //TODO: remove this and move size changes to State.. 26 | /// Callback when canvas size is changed. 27 | final bool Function(Size size)? onSize; 28 | 29 | /// Draws path based on data from [control]. 30 | const HandSignaturePaint({ 31 | Key? key, 32 | required this.control, 33 | this.color = Colors.black, 34 | this.strokeWidth = 1.0, 35 | this.maxStrokeWidth = 10.0, 36 | this.type = SignatureDrawType.shape, 37 | this.onSize, 38 | }) : super(key: key); 39 | 40 | @override 41 | _HandSignaturePaintState createState() => _HandSignaturePaintState(); 42 | } 43 | 44 | /// State of [HandSignaturePaint]. 45 | /// Subscribes to [HandSignatureControl] and rebuilds whenever signature data are changed. 46 | class _HandSignaturePaintState extends State { 47 | @override 48 | void initState() { 49 | super.initState(); 50 | 51 | widget.control.params = SignaturePaintParams( 52 | color: widget.color, 53 | strokeWidth: widget.strokeWidth, 54 | maxStrokeWidth: widget.maxStrokeWidth, 55 | ); 56 | 57 | widget.control.addListener(_updateState); 58 | } 59 | 60 | void _updateState() { 61 | setState(() {}); 62 | } 63 | 64 | @override 65 | void didUpdateWidget(HandSignaturePaint oldWidget) { 66 | super.didUpdateWidget(oldWidget); 67 | 68 | if (oldWidget.control != widget.control) { 69 | oldWidget.control.removeListener(_updateState); 70 | widget.control.addListener(_updateState); 71 | } 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return CustomPaint( 77 | painter: PathSignaturePainter( 78 | paths: widget.control.paths, 79 | color: widget.color, 80 | width: widget.strokeWidth, 81 | maxWidth: widget.maxStrokeWidth, 82 | type: widget.type, 83 | onSize: widget.onSize, 84 | ), 85 | ); 86 | } 87 | 88 | @override 89 | void dispose() { 90 | super.dispose(); 91 | 92 | widget.control.removeListener(_updateState); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/signature_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../signature.dart'; 4 | import 'utils.dart'; 5 | 6 | /// Type of signature path. 7 | /// [line] - simple line with constant size. 8 | /// [arc] - nicest, but worst performance. Creates thousands of small arcs. 9 | /// [shape] - every part of line is created by closed path and filled. Looks good and also have great performance. 10 | enum SignatureDrawType { 11 | line, 12 | arc, 13 | shape, 14 | } 15 | 16 | /// [CustomPainter] of [CubicPath]. 17 | /// Used during signature painting. 18 | class PathSignaturePainter extends CustomPainter { 19 | /// Paths to paint. 20 | final List paths; 21 | 22 | /// Single color of paint. 23 | final Color color; 24 | 25 | /// Minimal size of path. 26 | final double width; 27 | 28 | /// Maximal size of path. 29 | final double maxWidth; 30 | 31 | //TODO: remove this and move size changes to Widget side.. 32 | /// Callback when canvas size is changed. 33 | final bool Function(Size size)? onSize; 34 | 35 | /// Type of signature path. 36 | final SignatureDrawType type; 37 | 38 | /// Returns [PaintingStyle.stroke] based paint. 39 | Paint get strokePaint => Paint() 40 | ..color = color 41 | ..style = PaintingStyle.stroke 42 | ..strokeCap = StrokeCap.round 43 | ..strokeJoin = StrokeJoin.round 44 | ..strokeWidth = width; 45 | 46 | /// Returns [PaintingStyle.fill] based paint. 47 | Paint get fillPaint => Paint() 48 | ..color = color 49 | ..strokeWidth = 0.0; 50 | 51 | /// [Path] painter. 52 | PathSignaturePainter({ 53 | required this.paths, 54 | this.color = Colors.black, 55 | this.width = 1.0, 56 | this.maxWidth = 10.0, 57 | this.onSize, 58 | this.type = SignatureDrawType.shape, 59 | }); 60 | 61 | @override 62 | void paint(Canvas canvas, Size size) { 63 | //TODO: move to widget/state 64 | if (onSize != null) { 65 | if (onSize!.call(size)) { 66 | return; 67 | } 68 | } 69 | 70 | if (paths.isEmpty) { 71 | return; 72 | } 73 | 74 | switch (type) { 75 | case SignatureDrawType.line: 76 | final paint = strokePaint; 77 | 78 | paths.forEach((path) { 79 | if (path.isFilled) { 80 | canvas.drawPath(PathUtil.toLinePath(path.lines), paint); 81 | } 82 | }); 83 | break; 84 | case SignatureDrawType.arc: 85 | final paint = strokePaint; 86 | 87 | paths.forEach((path) { 88 | path.arcs.forEach((arc) { 89 | paint.strokeWidth = width + (maxWidth - width) * arc.size; 90 | canvas.drawPath(arc.path, paint); 91 | }); 92 | }); 93 | break; 94 | case SignatureDrawType.shape: 95 | final paint = fillPaint; 96 | 97 | paths.forEach((path) { 98 | if (path.isFilled) { 99 | if (path.isDot) { 100 | canvas.drawCircle(path.lines[0], 101 | path.lines[0].startRadius(width, maxWidth), paint); 102 | } else { 103 | canvas.drawPath( 104 | PathUtil.toShapePath(path.lines, width, maxWidth), paint); 105 | 106 | final first = path.lines.first; 107 | final last = path.lines.last; 108 | 109 | canvas.drawCircle( 110 | first.start, first.startRadius(width, maxWidth), paint); 111 | canvas.drawCircle( 112 | last.end, last.endRadius(width, maxWidth), paint); 113 | } 114 | } 115 | }); 116 | 117 | break; 118 | } 119 | } 120 | 121 | @override 122 | bool shouldRepaint(CustomPainter oldDelegate) { 123 | return true; 124 | } 125 | } 126 | 127 | class DebugSignaturePainterCP extends CustomPainter { 128 | final HandSignatureControl control; 129 | final bool cp; 130 | final bool cpStart; 131 | final bool cpEnd; 132 | final bool dot; 133 | final Color color; 134 | 135 | DebugSignaturePainterCP({ 136 | required this.control, 137 | this.cp = false, 138 | this.cpStart = true, 139 | this.cpEnd = true, 140 | this.dot = true, 141 | this.color = Colors.red, 142 | }); 143 | 144 | @override 145 | void paint(Canvas canvas, Size size) { 146 | final paint = Paint() 147 | ..color = color 148 | ..style = PaintingStyle.stroke 149 | ..strokeCap = StrokeCap.round 150 | ..strokeJoin = StrokeJoin.round 151 | ..strokeWidth = 1.0; 152 | 153 | control.lines.forEach((line) { 154 | if (cpStart) { 155 | canvas.drawLine(line.start, line.cpStart, paint); 156 | if (dot) { 157 | canvas.drawCircle(line.cpStart, 1.0, paint); 158 | canvas.drawCircle(line.start, 1.0, paint); 159 | } 160 | } else if (cp) { 161 | canvas.drawCircle(line.cpStart, 1.0, paint); 162 | } 163 | 164 | if (cpEnd) { 165 | canvas.drawLine(line.end, line.cpEnd, paint); 166 | if (dot) { 167 | canvas.drawCircle(line.cpEnd, 1.0, paint); 168 | } 169 | } 170 | }); 171 | } 172 | 173 | @override 174 | bool shouldRepaint(CustomPainter oldDelegate) { 175 | return true; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /lib/src/signature_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../signature.dart'; 5 | 6 | /// Wraps [HandSignaturePaint] to paint signature. And [RawGestureDetector] to send input to [HandSignatureControl]. 7 | class HandSignature extends StatelessWidget { 8 | /// Controls path creation. 9 | final HandSignatureControl control; 10 | 11 | /// Colors of path. 12 | final Color color; 13 | 14 | /// Minimal size of path. 15 | final double width; 16 | 17 | /// Maximal size of path. 18 | final double maxWidth; 19 | 20 | /// Path type. 21 | final SignatureDrawType type; 22 | 23 | /// The set of pointer device types to recognize, e.g., touch, stylus. 24 | /// Example: 25 | /// ``` 26 | /// supportedDevices: { 27 | /// PointerDeviceKind.stylus, 28 | /// } 29 | /// ``` 30 | /// If null, it accepts all pointer devices. 31 | final Set? supportedDevices; 32 | 33 | /// Callback when path drawing starts. 34 | final VoidCallback? onPointerDown; 35 | 36 | /// Callback when path drawing ends. 37 | final VoidCallback? onPointerUp; 38 | 39 | /// Draws [Path] based on input and stores data in [control]. 40 | HandSignature({ 41 | Key? key, 42 | required this.control, 43 | this.color = Colors.black, 44 | this.width = 1.0, 45 | this.maxWidth = 10.0, 46 | this.type = SignatureDrawType.shape, 47 | this.onPointerDown, 48 | this.onPointerUp, 49 | this.supportedDevices, 50 | }) : super(key: key); 51 | 52 | void _startPath(Offset point) { 53 | if (!control.hasActivePath) { 54 | onPointerDown?.call(); 55 | control.startPath(point); 56 | } 57 | } 58 | 59 | void _endPath(Offset point) { 60 | if (control.hasActivePath) { 61 | control.closePath(); 62 | onPointerUp?.call(); 63 | } 64 | } 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | return ClipRRect( 69 | child: RawGestureDetector( 70 | gestures: { 71 | _SingleGestureRecognizer: 72 | GestureRecognizerFactoryWithHandlers<_SingleGestureRecognizer>( 73 | () => _SingleGestureRecognizer( 74 | debugOwner: this, supportedDevices: supportedDevices), 75 | (instance) { 76 | instance.onStart = (position) => _startPath(position); 77 | instance.onUpdate = (position) => control.alterPath(position); 78 | instance.onEnd = (position) => _endPath(position); 79 | }, 80 | ), 81 | }, 82 | child: HandSignaturePaint( 83 | control: control, 84 | color: color, 85 | strokeWidth: width, 86 | maxStrokeWidth: maxWidth, 87 | type: type, 88 | onSize: control.notifyDimension, 89 | ), 90 | ), 91 | ); 92 | } 93 | } 94 | 95 | /// [GestureRecognizer] that allows just one input pointer. 96 | class _SingleGestureRecognizer extends OneSequenceGestureRecognizer { 97 | @override 98 | String get debugDescription => 'single_gesture_recognizer'; 99 | 100 | ValueChanged? onStart; 101 | ValueChanged? onUpdate; 102 | ValueChanged? onEnd; 103 | 104 | bool pointerActive = false; 105 | 106 | _SingleGestureRecognizer({ 107 | super.debugOwner, 108 | Set? supportedDevices, 109 | }) : super( 110 | supportedDevices: 111 | supportedDevices ?? PointerDeviceKind.values.toSet(), 112 | ); 113 | 114 | @override 115 | void addAllowedPointer(PointerDownEvent event) { 116 | if (pointerActive) { 117 | return; 118 | } 119 | 120 | startTrackingPointer(event.pointer, event.transform); 121 | } 122 | 123 | @override 124 | void handleEvent(PointerEvent event) { 125 | if (event is PointerMoveEvent) { 126 | onUpdate?.call(event.localPosition); 127 | } else if (event is PointerDownEvent) { 128 | pointerActive = true; 129 | onStart?.call(event.localPosition); 130 | } else if (event is PointerUpEvent) { 131 | pointerActive = false; 132 | onEnd?.call(event.localPosition); 133 | } else if (event is PointerCancelEvent) { 134 | pointerActive = false; 135 | onEnd?.call(event.localPosition); 136 | } 137 | } 138 | 139 | @override 140 | void didStopTrackingLastPointer(int pointer) {} 141 | } 142 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | import '../signature.dart'; 6 | 7 | extension ColorEx on Color { 8 | String get hexValue => '#${value.toRadixString(16)}'.replaceRange(1, 3, ''); 9 | } 10 | 11 | extension OffsetEx on Offset { 12 | Offset axisDistanceTo(Offset other) => other - this; 13 | 14 | double distanceTo(Offset other) { 15 | final len = axisDistanceTo(other); 16 | 17 | return sqrt(len.dx * len.dx + len.dy * len.dy); 18 | } 19 | 20 | double angleTo(Offset other) { 21 | final len = axisDistanceTo(other); 22 | 23 | return atan2(len.dy, len.dx); 24 | } 25 | 26 | Offset directionTo(Offset other) { 27 | final len = axisDistanceTo(other); 28 | final m = sqrt(len.dx * len.dx + len.dy * len.dy); 29 | 30 | return Offset(m == 0 ? 0 : (len.dx / m), m == 0 ? 0 : (len.dy / m)); 31 | } 32 | 33 | Offset rotate(double radians) { 34 | final s = sin(radians); 35 | final c = cos(radians); 36 | 37 | final x = dx * c - dy * s; 38 | final y = dx * s + dy * c; 39 | 40 | return Offset(x, y); 41 | } 42 | 43 | Offset rotateAround(Offset center, double radians) { 44 | return (this - center).rotate(radians) + center; 45 | } 46 | } 47 | 48 | extension PathEx on Path { 49 | void start(Offset offset) => moveTo(offset.dx, offset.dy); 50 | 51 | void cubic(Offset cpStart, Offset cpEnd, Offset end) => 52 | cubicTo(cpStart.dx, cpStart.dy, cpEnd.dx, cpEnd.dy, end.dx, end.dy); 53 | 54 | void line(Offset offset) => lineTo(offset.dx, offset.dy); 55 | } 56 | 57 | extension SizeExt on Size { 58 | Size scaleToFit(Size other) { 59 | final scale = min( 60 | other.width / this.width, 61 | other.height / this.height, 62 | ); 63 | 64 | return this * scale; 65 | } 66 | } 67 | 68 | //TODO: clean up 69 | class PathUtil { 70 | static Rect bounds(List data, 71 | {double minSize = 2.0, double radius = 0.0}) { 72 | double left = data[0].dx; 73 | double top = data[0].dy; 74 | double right = data[0].dx; 75 | double bottom = data[0].dy; 76 | 77 | data.forEach((point) { 78 | final x = point.dx; 79 | final y = point.dy; 80 | 81 | if (x < left) { 82 | left = x; 83 | } else if (x > right) { 84 | right = x; 85 | } 86 | 87 | if (y < top) { 88 | top = y; 89 | } else if (y > bottom) { 90 | bottom = y; 91 | } 92 | }); 93 | 94 | final hSize = right - left; 95 | final vSize = bottom - top; 96 | 97 | if (hSize < minSize) { 98 | final dif = (minSize - hSize) * 0.5; 99 | left -= dif; 100 | right += dif; 101 | } 102 | 103 | if (vSize < minSize) { 104 | final dif = (minSize - vSize) * 0.5; 105 | top -= dif; 106 | bottom += dif; 107 | } 108 | 109 | return Rect.fromLTRB( 110 | left - radius, top - radius, right + radius, bottom + radius); 111 | } 112 | 113 | static Rect boundsOf(List> data, 114 | {double minSize = 2.0, double radius = 0.0}) { 115 | double left = data[0][0].dx; 116 | double top = data[0][0].dy; 117 | double right = data[0][0].dx; 118 | double bottom = data[0][0].dy; 119 | 120 | data.forEach((set) => set.forEach((point) { 121 | final x = point.dx; 122 | final y = point.dy; 123 | 124 | if (x < left) { 125 | left = x; 126 | } else if (x > right) { 127 | right = x; 128 | } 129 | 130 | if (y < top) { 131 | top = y; 132 | } else if (y > bottom) { 133 | bottom = y; 134 | } 135 | })); 136 | 137 | final hSize = right - left; 138 | final vSize = bottom - top; 139 | 140 | if (hSize < minSize) { 141 | final dif = (minSize - hSize) * 0.5; 142 | left -= dif; 143 | right += dif; 144 | } 145 | 146 | if (vSize < minSize) { 147 | final dif = (minSize - vSize) * 0.5; 148 | top -= dif; 149 | bottom += dif; 150 | } 151 | 152 | return Rect.fromLTRB( 153 | left - radius, top - radius, right + radius, bottom + radius); 154 | } 155 | 156 | static List translate(List data, Offset location) { 157 | final output = []; 158 | 159 | data.forEach( 160 | (point) => output.add(point.translate(location.dx, location.dy) as T)); 161 | 162 | return output; 163 | } 164 | 165 | static List> translateData( 166 | List> data, Offset location) { 167 | final output = >[]; 168 | 169 | data.forEach((set) => output.add(translate(set, location))); 170 | 171 | return output; 172 | } 173 | 174 | static List scale(List data, double ratio) { 175 | final output = []; 176 | 177 | data.forEach((point) => output.add(point.scale(ratio, ratio) as T)); 178 | 179 | return output; 180 | } 181 | 182 | static List> scaleData( 183 | List> data, double ratio) { 184 | final output = >[]; 185 | 186 | data.forEach((set) => output.add(scale(set, ratio))); 187 | 188 | return output; 189 | } 190 | 191 | static List normalize(List data, {Rect? bound}) { 192 | bound ??= bounds(data); 193 | 194 | return scale( 195 | translate(data, -bound.topLeft), 196 | 1.0 / max(bound.width, bound.height), 197 | ); 198 | } 199 | 200 | static List> normalizeData(List> data, 201 | {Rect? bound}) { 202 | bound ??= boundsOf(data); 203 | 204 | final ratio = 1.0 / max(bound.width, bound.height); 205 | 206 | return scaleData( 207 | translateData(data, -bound.topLeft), 208 | ratio, 209 | ); 210 | } 211 | 212 | static List fill(List data, Rect rect, 213 | {double radius = 0.0, Rect? bound, double border = 32.0}) { 214 | bound ??= bounds(data, radius: radius); 215 | border *= 2.0; 216 | 217 | final outputSize = Size(rect.width - border, rect.height - border); 218 | final sourceSize = Size(bound.width, bound.height); 219 | Size destinationSize; 220 | 221 | final wr = outputSize.width / sourceSize.width; 222 | final hr = outputSize.height / sourceSize.height; 223 | 224 | if (wr < hr) { 225 | //scale by width 226 | destinationSize = Size(outputSize.width, sourceSize.height * wr); 227 | } else { 228 | //scale by height 229 | destinationSize = Size(sourceSize.width * hr, outputSize.height); 230 | } 231 | 232 | final borderSize = Offset(outputSize.width - destinationSize.width + border, 233 | outputSize.height - destinationSize.height + border) * 234 | 0.5; 235 | 236 | return translate( 237 | scale( 238 | normalize(data, bound: bound), 239 | max(destinationSize.width, destinationSize.height), 240 | ), 241 | borderSize, 242 | ); 243 | } 244 | 245 | static List> fillData(List> data, Rect rect, 246 | {Rect? bound, double? border}) { 247 | bound ??= boundsOf(data); 248 | border ??= 4.0; 249 | 250 | final outputSize = rect.size; 251 | final sourceSize = bound; 252 | Size destinationSize; 253 | 254 | if (outputSize.width / outputSize.height > 255 | sourceSize.width / sourceSize.height) { 256 | destinationSize = Size( 257 | sourceSize.width * outputSize.height / sourceSize.height, 258 | outputSize.height); 259 | } else { 260 | destinationSize = Size(outputSize.width, 261 | sourceSize.height * outputSize.width / sourceSize.width); 262 | } 263 | 264 | destinationSize = Size(destinationSize.width - border * 2.0, 265 | destinationSize.height - border * 2.0); 266 | final borderSize = Offset(rect.width - destinationSize.width, 267 | rect.height - destinationSize.height) * 268 | 0.5; 269 | 270 | return translateData( 271 | scaleData( 272 | normalizeData(data, bound: bound), 273 | max(destinationSize.width, destinationSize.height), 274 | ), 275 | borderSize); 276 | } 277 | 278 | static Path toPath(List points) { 279 | final path = Path(); 280 | 281 | if (points.length > 0) { 282 | path.moveTo(points[0].dx, points[0].dy); 283 | points.forEach((point) => path.lineTo(point.dx, point.dy)); 284 | } 285 | 286 | return path; 287 | } 288 | 289 | static List toPaths(List> data) { 290 | final paths = []; 291 | 292 | data.forEach((line) => paths.add(toPath(line))); 293 | 294 | return paths; 295 | } 296 | 297 | static Rect pathBounds(List data) { 298 | Rect init = data[0].getBounds(); 299 | 300 | double left = init.left; 301 | double top = init.top; 302 | double right = init.right; 303 | double bottom = init.bottom; 304 | 305 | data.forEach((path) { 306 | final bound = path.getBounds(); 307 | 308 | left = min(left, bound.left); 309 | top = min(top, bound.top); 310 | right = max(right, bound.right); 311 | bottom = max(bottom, bound.bottom); 312 | }); 313 | 314 | return Rect.fromLTRB(left, top, right, bottom); 315 | } 316 | 317 | static Path scalePath(Path data, double ratio) { 318 | final transform = Matrix4.identity(); 319 | transform.scale(ratio, ratio); 320 | 321 | return data.transform(transform.storage); 322 | } 323 | 324 | static List scalePaths(List data, double ratio) { 325 | final output = []; 326 | 327 | data.forEach((path) => output.add(scalePath(path, ratio))); 328 | 329 | return output; 330 | } 331 | 332 | static List translatePaths(List data, Offset location) { 333 | final output = []; 334 | 335 | final transform = Matrix4.identity(); 336 | transform.translate(location.dx, location.dy); 337 | 338 | data.forEach((path) => output.add(path.transform(transform.storage))); 339 | 340 | return output; 341 | } 342 | 343 | static Path toShapePath(List lines, double size, double maxSize) { 344 | assert(lines.length > 0); 345 | 346 | if (lines.length == 1) { 347 | final line = lines[0]; 348 | if (line.isDot) { 349 | //TODO: return null or create circle ? 350 | return Path() 351 | ..start(line.start) 352 | ..line(line.end); 353 | } 354 | 355 | return line.toShape(size, maxSize); 356 | } 357 | 358 | final path = Path(); 359 | 360 | final firstLine = lines.first; 361 | path.start(firstLine.start + firstLine.cpsUp(size, maxSize)); 362 | 363 | for (int i = 0; i < lines.length; i++) { 364 | final line = lines[i]; 365 | final d1 = line.cpsUp(size, maxSize); 366 | final d2 = line.cpeUp(size, maxSize); 367 | 368 | path.cubic(line.cpStart + d1, line.cpEnd + d2, line.end + d2); 369 | } 370 | 371 | final lastLine = lines.last; 372 | path.line(lastLine.end + lastLine.cpeDown(size, maxSize)); 373 | 374 | for (int i = lines.length - 1; i > -1; i--) { 375 | final line = lines[i]; 376 | final d3 = line.cpeDown(size, maxSize); 377 | final d4 = line.cpsDown(size, maxSize); 378 | 379 | path.cubic(line.cpEnd + d3, line.cpStart + d4, line.start + d4); 380 | } 381 | 382 | path.close(); 383 | 384 | return path; 385 | } 386 | 387 | static Path toLinePath(List lines) { 388 | assert(lines.length > 0); 389 | 390 | final path = Path()..start(lines[0]); 391 | 392 | lines.forEach((line) => path.cubic(line.cpStart, line.cpEnd, line.end)); 393 | 394 | return path; 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: hand_signature 2 | description: The Signature Pad Widget that allows you to draw smooth signatures. With variety of draw and export settings. And also supports SVG. 3 | homepage: https://github.com/romanbase/hand_signature 4 | version: 3.0.3 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | dev_dependencies: 14 | flutter_test: 15 | sdk: flutter 16 | 17 | flutter: 18 | -------------------------------------------------------------------------------- /test/signature_control_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:hand_signature/signature.dart'; 6 | 7 | void main() { 8 | WidgetsFlutterBinding.ensureInitialized(); 9 | 10 | final control = HandSignatureControl(); 11 | 12 | // mock curve sequence 13 | control.startPath(OffsetPoint(dx: 0.0, dy: 0.0, timestamp: 1)); 14 | control.alterPath(OffsetPoint(dx: 10.0, dy: 10.0, timestamp: 10)); 15 | control.alterPath(OffsetPoint(dx: 20.0, dy: 20.0, timestamp: 15)); 16 | control.alterPath(OffsetPoint(dx: 30.0, dy: 20.0, timestamp: 20)); 17 | control.closePath(); 18 | 19 | // mock dot sequence 20 | control.startPath(OffsetPoint(dx: 30.0, dy: 30.0, timestamp: 25)); 21 | control.closePath(); 22 | 23 | // json string representing above mock data 24 | final json = 25 | '[[{"x":0.0,"y":0.0,"t":1},{"x":10.0,"y":10.0,"t":10},{"x":20.0,"y":20.0,"t":15},{"x":30.0,"y":20.0,"t":20}],[{"x":30.0,"y":30.0,"t":25}]]'; 26 | 27 | group('IO', () { 28 | test('points', () async { 29 | final paths = control.paths; 30 | final curve = paths[0]; 31 | final dot = paths[1]; 32 | 33 | expect(paths.length, 2); 34 | expect(curve.points.length, 4); 35 | expect(curve.lines.length, 3); 36 | 37 | // velocity of first line should be lower because second line is drawn faster while distance is identical 38 | expect(curve.lines[0].end - curve.lines[0].start, 39 | equals(curve.lines[1].end - curve.lines[1].start)); 40 | expect(curve.lines[0].velocity(), lessThan(curve.lines[1].velocity())); 41 | 42 | expect(dot.points.length, 1); 43 | expect(dot.isDot, isTrue); 44 | }); 45 | 46 | test('export', () async { 47 | final paths = control.paths; 48 | 49 | final export = 50 | '[${paths.map((e) => '[${e.points.map((e) => '{"x":${e.dx},"y":${e.dy},"t":${e.timestamp}}').join(',')}]').join(',')}]'; 51 | final data = jsonDecode(export); 52 | 53 | expect(data, isNotNull); 54 | expect((data as List).length, 2); 55 | expect((data[0] as List).length, 4); 56 | expect((data[1] as List).length, 1); 57 | 58 | expect(export, equals(json)); 59 | }); 60 | 61 | test('import', () async { 62 | final controlIn = HandSignatureControl(); 63 | 64 | final data = jsonDecode(json) as Iterable; 65 | 66 | data.forEach((element) { 67 | final line = List.of(element); 68 | expect(line.length, greaterThan(0)); 69 | 70 | //start path with first point 71 | controlIn.startPath(OffsetPoint( 72 | dx: line[0]['x'], 73 | dy: line[0]['y'], 74 | timestamp: line[0]['t'], 75 | )); 76 | 77 | //skip first point and alter path with rest of points 78 | line.skip(1).forEach((item) { 79 | controlIn.alterPath(OffsetPoint( 80 | dx: item['x'], 81 | dy: item['y'], 82 | timestamp: item['t'], 83 | )); 84 | }); 85 | 86 | //close path 87 | controlIn.closePath(); 88 | }); 89 | 90 | final paths = controlIn.paths; 91 | final curve = paths[0]; 92 | final dot = paths[1]; 93 | 94 | expect(paths.length, 2); 95 | expect(curve.points.length, 4); 96 | expect(curve.lines.length, 3); 97 | 98 | // velocity of first line is lower because second line is drawn faster while distance is identical 99 | expect(curve.lines[0].end - curve.lines[0].start, 100 | equals(curve.lines[1].end - curve.lines[1].start)); 101 | expect(curve.lines[0].velocity(), lessThan(curve.lines[1].velocity())); 102 | 103 | expect(dot.points.length, 1); 104 | expect(dot.isDot, isTrue); 105 | 106 | // check equality of individual OffsetPoints of CubePaths 107 | expect(controlIn.equals(control), isTrue); 108 | }); 109 | 110 | test('map', () async { 111 | final controlMap = HandSignatureControl(); 112 | controlMap.importData(control.toMap()); 113 | 114 | // check equality of individual OffsetPoints of CubePaths 115 | expect(controlMap.equals(control), isTrue); 116 | }); 117 | 118 | test('image', () async { 119 | final controlImage = HandSignatureControl(); 120 | 121 | controlImage.importData(control.toMap()); 122 | controlImage.notifyDimension(Size(1280, 720)); 123 | 124 | final image = await controlImage.toImage(); 125 | 126 | expect(image, isNotNull); 127 | 128 | final data = image!.buffer.asUint8List(); 129 | 130 | expect(data, isNotNull); 131 | }); 132 | }); 133 | } 134 | --------------------------------------------------------------------------------