ESP32 | BLUETOOTH CLASSIC | Flutter - Let's build BT Serial based on the examples. (Ft. Chat App)
@@ -0,0 +1,30 @@
|
||||
//https://github.com/espressif/arduino-esp32/blob/master/libraries/BluetoothSerial/examples/SerialToSerialBT/SerialToSerialBT.ino
|
||||
//This example code is in the Public Domain (or CC0 licensed, at your option.)
|
||||
//By Evandro Copercini - 2018
|
||||
//
|
||||
//This example creates a bridge between Serial and Classical Bluetooth (SPP)
|
||||
//and also demonstrate that SerialBT have the same functionalities of a normal Serial
|
||||
|
||||
#include "BluetoothSerial.h"
|
||||
|
||||
#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
|
||||
#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
|
||||
#endif
|
||||
|
||||
BluetoothSerial SerialBT;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
SerialBT.begin("ESP32_CLASSIC_BT"); //Bluetooth device name
|
||||
Serial.println("The device started, now you can pair it with bluetooth!");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (Serial.available()) {
|
||||
SerialBT.write(Serial.read());
|
||||
}
|
||||
if (SerialBT.available()) {
|
||||
Serial.write(SerialBT.read());
|
||||
}
|
||||
delay(20);
|
||||
}
|
||||
44
ESP32_BT_CLASSIC/ESP32_BLUETOOTH_SERIAL_DEMO/android_bluetooth_serial_app/.gitignore
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Exceptions to above rules.
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
@@ -0,0 +1,10 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 45da0f63af4fbc60b920c3a13d58201408c3c8f4
|
||||
channel: master
|
||||
|
||||
project_type: app
|
||||
@@ -0,0 +1,16 @@
|
||||
# androidbluetoothserialapp
|
||||
|
||||
A new Flutter application.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
|
||||
|
||||
For help getting started with Flutter, view our
|
||||
[online documentation](https://flutter.dev/docs), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
7
ESP32_BT_CLASSIC/ESP32_BLUETOOTH_SERIAL_DEMO/android_bluetooth_serial_app/android/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
@@ -0,0 +1,54 @@
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.thatproject.androidbluetoothserialapp"
|
||||
minSdkVersion 18
|
||||
targetSdkVersion 28
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.thatproject.androidbluetoothserialapp">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,47 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.thatproject.androidbluetoothserialapp">
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
In most cases you can leave this as-is, but you if you want to provide
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="androidbluetoothserialapp"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<!-- Displays an Android View that continues showing the launch screen
|
||||
Drawable until Flutter paints its first frame, then this splash
|
||||
screen fades out. A splash screen is useful to avoid any visual
|
||||
gap between the end of Android's launch screen and the painting of
|
||||
Flutter's first frame. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.thatproject.androidbluetoothserialapp;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">@android:color/white</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.thatproject.androidbluetoothserialapp">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,29 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.enableR8=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
@@ -0,0 +1,6 @@
|
||||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
32
ESP32_BT_CLASSIC/ESP32_BLUETOOTH_SERIAL_DEMO/android_bluetooth_serial_app/ios/.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>8.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,2 @@
|
||||
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,2 @@
|
||||
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,84 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '9.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def parse_KV_file(file, separator='=')
|
||||
file_abs_path = File.expand_path(file)
|
||||
if !File.exists? file_abs_path
|
||||
return [];
|
||||
end
|
||||
generated_key_values = {}
|
||||
skip_line_start_symbols = ["#", "/"]
|
||||
File.foreach(file_abs_path) do |line|
|
||||
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
|
||||
plugin = line.split(pattern=separator)
|
||||
if plugin.length == 2
|
||||
podname = plugin[0].strip()
|
||||
path = plugin[1].strip()
|
||||
podpath = File.expand_path("#{path}", file_abs_path)
|
||||
generated_key_values[podname] = podpath
|
||||
else
|
||||
puts "Invalid plugin specification: #{line}"
|
||||
end
|
||||
end
|
||||
generated_key_values
|
||||
end
|
||||
|
||||
target 'Runner' do
|
||||
# Flutter Pod
|
||||
|
||||
copied_flutter_dir = File.join(__dir__, 'Flutter')
|
||||
copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
|
||||
copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
|
||||
unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
|
||||
# Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
|
||||
# That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
|
||||
# CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
|
||||
|
||||
generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
|
||||
cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
|
||||
|
||||
unless File.exist?(copied_framework_path)
|
||||
FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
|
||||
end
|
||||
unless File.exist?(copied_podspec_path)
|
||||
FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
|
||||
end
|
||||
end
|
||||
|
||||
# Keep pod path relative so it can be checked into Podfile.lock.
|
||||
pod 'Flutter', :path => 'Flutter'
|
||||
|
||||
# Plugin Pods
|
||||
|
||||
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
|
||||
# referring to absolute paths on developers' machines.
|
||||
system('rm -rf .symlinks')
|
||||
system('mkdir -p .symlinks/plugins')
|
||||
plugin_pods = parse_KV_file('../.flutter-plugins')
|
||||
plugin_pods.each do |name, path|
|
||||
symlink = File.join('.symlinks', 'plugins', name)
|
||||
File.symlink(path, symlink)
|
||||
pod name, :path => File.join(symlink, 'ios')
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['ENABLE_BITCODE'] = 'NO'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,496 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
|
||||
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
||||
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
|
||||
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
97C146F11CF9000F007C117D /* Supporting Files */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F11CF9000F007C117D /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146F21CF9000F007C117D /* main.m */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1020;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
|
||||
97C146F31CF9000F007C117D /* main.m in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thatproject.androidbluetoothserialapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thatproject.androidbluetoothserialapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.thatproject.androidbluetoothserialapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,6 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : FlutterAppDelegate
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
#import "AppDelegate.h"
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (BOOL)application:(UIApplication *)application
|
||||
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
[GeneratedPluginRegistrant registerWithRegistry:self];
|
||||
// Override point for customization after application launch.
|
||||
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 564 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,5 @@
|
||||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>androidbluetoothserialapp</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,9 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "AppDelegate.h"
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
@autoreleasepool {
|
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import './BackgroundCollectingTask.dart';
|
||||
import './helpers/LineChart.dart';
|
||||
import './helpers/PaintStyle.dart';
|
||||
|
||||
class BackgroundCollectedPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final BackgroundCollectingTask task =
|
||||
BackgroundCollectingTask.of(context, rebuildOnChange: true);
|
||||
|
||||
// Arguments shift is needed for timestamps as miliseconds in double could loose precision.
|
||||
final int argumentsShift =
|
||||
task.samples.first.timestamp.millisecondsSinceEpoch;
|
||||
|
||||
final Duration showDuration =
|
||||
Duration(hours: 2); // @TODO . show duration should be configurable
|
||||
final Iterable<DataSample> lastSamples = task.getLastOf(showDuration);
|
||||
|
||||
final Iterable<double> arguments = lastSamples.map((sample) {
|
||||
return (sample.timestamp.millisecondsSinceEpoch - argumentsShift)
|
||||
.toDouble();
|
||||
});
|
||||
|
||||
// Step for argument labels
|
||||
final Duration argumentsStep =
|
||||
Duration(minutes: 15); // @TODO . step duration should be configurable
|
||||
|
||||
// Find first timestamp floored to step before
|
||||
final DateTime beginningArguments = lastSamples.first.timestamp;
|
||||
DateTime beginningArgumentsStep = DateTime(beginningArguments.year,
|
||||
beginningArguments.month, beginningArguments.day);
|
||||
while (beginningArgumentsStep.isBefore(beginningArguments)) {
|
||||
beginningArgumentsStep = beginningArgumentsStep.add(argumentsStep);
|
||||
}
|
||||
beginningArgumentsStep = beginningArgumentsStep.subtract(argumentsStep);
|
||||
final DateTime endingArguments = lastSamples.last.timestamp;
|
||||
|
||||
// Generate list of timestamps of labels
|
||||
final Iterable<DateTime> argumentsLabelsTimestamps = () sync* {
|
||||
DateTime timestamp = beginningArgumentsStep;
|
||||
yield timestamp;
|
||||
while (timestamp.isBefore(endingArguments)) {
|
||||
timestamp = timestamp.add(argumentsStep);
|
||||
yield timestamp;
|
||||
}
|
||||
}();
|
||||
|
||||
// Map strings for labels
|
||||
final Iterable<LabelEntry> argumentsLabels =
|
||||
argumentsLabelsTimestamps.map((timestamp) {
|
||||
return LabelEntry(
|
||||
(timestamp.millisecondsSinceEpoch - argumentsShift).toDouble(),
|
||||
((timestamp.hour <= 9 ? '0' : '') +
|
||||
timestamp.hour.toString() +
|
||||
':' +
|
||||
(timestamp.minute <= 9 ? '0' : '') +
|
||||
timestamp.minute.toString()));
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Collected data'),
|
||||
actions: <Widget>[
|
||||
// Progress circle
|
||||
(task.inProgress
|
||||
? FittedBox(
|
||||
child: Container(
|
||||
margin: new EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Colors.white))))
|
||||
: Container(/* Dummy */)),
|
||||
// Start/stop buttons
|
||||
(task.inProgress
|
||||
? IconButton(icon: Icon(Icons.pause), onPressed: task.pause)
|
||||
: IconButton(
|
||||
icon: Icon(Icons.play_arrow), onPressed: task.reasume)),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.brightness_7),
|
||||
title: const Text('Temperatures'),
|
||||
subtitle: const Text('In Celsius'),
|
||||
),
|
||||
LineChart(
|
||||
constraints: const BoxConstraints.expand(height: 350),
|
||||
arguments: arguments,
|
||||
argumentsLabels: argumentsLabels,
|
||||
values: [
|
||||
lastSamples.map((sample) => sample.temperature1),
|
||||
lastSamples.map((sample) => sample.temperature2),
|
||||
],
|
||||
verticalLinesStyle: const PaintStyle(color: Colors.grey),
|
||||
additionalMinimalHorizontalLabelsInterval: 0,
|
||||
additionalMinimalVerticalLablesInterval: 0,
|
||||
seriesPointsStyles: [
|
||||
null,
|
||||
null,
|
||||
//const PaintStyle(style: PaintingStyle.stroke, strokeWidth: 1.7*3, color: Colors.indigo, strokeCap: StrokeCap.round),
|
||||
],
|
||||
seriesLinesStyles: [
|
||||
const PaintStyle(
|
||||
style: PaintingStyle.stroke,
|
||||
strokeWidth: 1.7,
|
||||
color: Colors.indigoAccent),
|
||||
const PaintStyle(
|
||||
style: PaintingStyle.stroke,
|
||||
strokeWidth: 1.7,
|
||||
color: Colors.redAccent),
|
||||
],
|
||||
),
|
||||
Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.filter_vintage),
|
||||
title: const Text('Water pH level'),
|
||||
),
|
||||
LineChart(
|
||||
constraints: const BoxConstraints.expand(height: 200),
|
||||
arguments: arguments,
|
||||
argumentsLabels: argumentsLabels,
|
||||
values: [
|
||||
lastSamples.map((sample) => sample.waterpHlevel),
|
||||
],
|
||||
verticalLinesStyle: const PaintStyle(color: Colors.grey),
|
||||
additionalMinimalHorizontalLabelsInterval: 0,
|
||||
additionalMinimalVerticalLablesInterval: 0,
|
||||
seriesPointsStyles: [
|
||||
null,
|
||||
],
|
||||
seriesLinesStyles: [
|
||||
const PaintStyle(
|
||||
style: PaintingStyle.stroke,
|
||||
strokeWidth: 1.7,
|
||||
color: Colors.greenAccent),
|
||||
],
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||
import 'package:scoped_model/scoped_model.dart';
|
||||
|
||||
class DataSample {
|
||||
double temperature1;
|
||||
double temperature2;
|
||||
double waterpHlevel;
|
||||
DateTime timestamp;
|
||||
|
||||
DataSample({
|
||||
this.temperature1,
|
||||
this.temperature2,
|
||||
this.waterpHlevel,
|
||||
this.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
class BackgroundCollectingTask extends Model {
|
||||
static BackgroundCollectingTask of(
|
||||
BuildContext context, {
|
||||
bool rebuildOnChange = false,
|
||||
}) =>
|
||||
ScopedModel.of<BackgroundCollectingTask>(
|
||||
context,
|
||||
rebuildOnChange: rebuildOnChange,
|
||||
);
|
||||
|
||||
final BluetoothConnection _connection;
|
||||
List<int> _buffer = List<int>();
|
||||
|
||||
// @TODO , Such sample collection in real code should be delegated
|
||||
// (via `Stream<DataSample>` preferably) and then saved for later
|
||||
// displaying on chart (or even stright prepare for displaying).
|
||||
// @TODO ? should be shrinked at some point, endless colleting data would cause memory shortage.
|
||||
List<DataSample> samples = List<DataSample>();
|
||||
|
||||
bool inProgress;
|
||||
|
||||
BackgroundCollectingTask._fromConnection(this._connection) {
|
||||
_connection.input.listen((data) {
|
||||
_buffer += data;
|
||||
|
||||
while (true) {
|
||||
// If there is a sample, and it is full sent
|
||||
int index = _buffer.indexOf('t'.codeUnitAt(0));
|
||||
if (index >= 0 && _buffer.length - index >= 7) {
|
||||
final DataSample sample = DataSample(
|
||||
temperature1: (_buffer[index + 1] + _buffer[index + 2] / 100),
|
||||
temperature2: (_buffer[index + 3] + _buffer[index + 4] / 100),
|
||||
waterpHlevel: (_buffer[index + 5] + _buffer[index + 6] / 100),
|
||||
timestamp: DateTime.now());
|
||||
_buffer.removeRange(0, index + 7);
|
||||
|
||||
samples.add(sample);
|
||||
notifyListeners(); // Note: It shouldn't be invoked very often - in this example data comes at every second, but if there would be more data, it should update (including repaint of graphs) in some fixed interval instead of after every sample.
|
||||
//print("${sample.timestamp.toString()} -> ${sample.temperature1} / ${sample.temperature2}");
|
||||
}
|
||||
// Otherwise break
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).onDone(() {
|
||||
inProgress = false;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
static Future<BackgroundCollectingTask> connect(
|
||||
BluetoothDevice server) async {
|
||||
final BluetoothConnection connection =
|
||||
await BluetoothConnection.toAddress(server.address);
|
||||
return BackgroundCollectingTask._fromConnection(connection);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_connection.dispose();
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
inProgress = true;
|
||||
_buffer.clear();
|
||||
samples.clear();
|
||||
notifyListeners();
|
||||
_connection.output.add(ascii.encode('start'));
|
||||
await _connection.output.allSent;
|
||||
}
|
||||
|
||||
Future<void> cancel() async {
|
||||
inProgress = false;
|
||||
notifyListeners();
|
||||
_connection.output.add(ascii.encode('stop'));
|
||||
await _connection.finish();
|
||||
}
|
||||
|
||||
Future<void> pause() async {
|
||||
inProgress = false;
|
||||
notifyListeners();
|
||||
_connection.output.add(ascii.encode('stop'));
|
||||
await _connection.output.allSent;
|
||||
}
|
||||
|
||||
Future<void> reasume() async {
|
||||
inProgress = true;
|
||||
notifyListeners();
|
||||
_connection.output.add(ascii.encode('start'));
|
||||
await _connection.output.allSent;
|
||||
}
|
||||
|
||||
Iterable<DataSample> getLastOf(Duration duration) {
|
||||
DateTime startingTime = DateTime.now().subtract(duration);
|
||||
int i = samples.length;
|
||||
do {
|
||||
i -= 1;
|
||||
if (i <= 0) {
|
||||
break;
|
||||
}
|
||||
} while (samples[i].timestamp.isAfter(startingTime));
|
||||
return samples.getRange(i, samples.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||
|
||||
class BluetoothDeviceListEntry extends ListTile {
|
||||
BluetoothDeviceListEntry({
|
||||
@required BluetoothDevice device,
|
||||
int rssi,
|
||||
GestureTapCallback onTap,
|
||||
GestureLongPressCallback onLongPress,
|
||||
bool enabled = true,
|
||||
}) : super(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
enabled: enabled,
|
||||
leading:
|
||||
Icon(Icons.devices), // @TODO . !BluetoothClass! class aware icon
|
||||
title: Text(device.name ?? "Unknown device"),
|
||||
subtitle: Text(device.address.toString()),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
rssi != null
|
||||
? Container(
|
||||
margin: new EdgeInsets.all(8.0),
|
||||
child: DefaultTextStyle(
|
||||
style: _computeTextStyle(rssi),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(rssi.toString()),
|
||||
Text('dBm'),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(width: 0, height: 0),
|
||||
device.isConnected
|
||||
? Icon(Icons.import_export)
|
||||
: Container(width: 0, height: 0),
|
||||
device.isBonded
|
||||
? Icon(Icons.link)
|
||||
: Container(width: 0, height: 0),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
static TextStyle _computeTextStyle(int rssi) {
|
||||
/**/ if (rssi >= -35)
|
||||
return TextStyle(color: Colors.greenAccent[700]);
|
||||
else if (rssi >= -45)
|
||||
return TextStyle(
|
||||
color: Color.lerp(
|
||||
Colors.greenAccent[700], Colors.lightGreen, -(rssi + 35) / 10));
|
||||
else if (rssi >= -55)
|
||||
return TextStyle(
|
||||
color: Color.lerp(
|
||||
Colors.lightGreen, Colors.lime[600], -(rssi + 45) / 10));
|
||||
else if (rssi >= -65)
|
||||
return TextStyle(
|
||||
color: Color.lerp(Colors.lime[600], Colors.amber, -(rssi + 55) / 10));
|
||||
else if (rssi >= -75)
|
||||
return TextStyle(
|
||||
color: Color.lerp(
|
||||
Colors.amber, Colors.deepOrangeAccent, -(rssi + 65) / 10));
|
||||
else if (rssi >= -85)
|
||||
return TextStyle(
|
||||
color: Color.lerp(
|
||||
Colors.deepOrangeAccent, Colors.redAccent, -(rssi + 75) / 10));
|
||||
else
|
||||
/*code symetry*/
|
||||
return TextStyle(color: Colors.redAccent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||
|
||||
class ChatPage extends StatefulWidget {
|
||||
final BluetoothDevice server;
|
||||
|
||||
const ChatPage({this.server});
|
||||
|
||||
@override
|
||||
_ChatPage createState() => new _ChatPage();
|
||||
}
|
||||
|
||||
class _Message {
|
||||
int whom;
|
||||
String text;
|
||||
|
||||
_Message(this.whom, this.text);
|
||||
}
|
||||
|
||||
class _ChatPage extends State<ChatPage> {
|
||||
static final clientID = 0;
|
||||
BluetoothConnection connection;
|
||||
|
||||
List<_Message> messages = List<_Message>();
|
||||
String _messageBuffer = '';
|
||||
|
||||
final TextEditingController textEditingController =
|
||||
new TextEditingController();
|
||||
final ScrollController listScrollController = new ScrollController();
|
||||
|
||||
bool isConnecting = true;
|
||||
bool get isConnected => connection != null && connection.isConnected;
|
||||
|
||||
bool isDisconnecting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
BluetoothConnection.toAddress(widget.server.address).then((_connection) {
|
||||
print('Connected to the device');
|
||||
connection = _connection;
|
||||
setState(() {
|
||||
isConnecting = false;
|
||||
isDisconnecting = false;
|
||||
});
|
||||
|
||||
connection.input.listen(_onDataReceived).onDone(() {
|
||||
// Example: Detect which side closed the connection
|
||||
// There should be `isDisconnecting` flag to show are we are (locally)
|
||||
// in middle of disconnecting process, should be set before calling
|
||||
// `dispose`, `finish` or `close`, which all causes to disconnect.
|
||||
// If we except the disconnection, `onDone` should be fired as result.
|
||||
// If we didn't except this (no flag set), it means closing by remote.
|
||||
if (isDisconnecting) {
|
||||
print('Disconnecting locally!');
|
||||
} else {
|
||||
print('Disconnected remotely!');
|
||||
}
|
||||
if (this.mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}).catchError((error) {
|
||||
print('Cannot connect, exception occured');
|
||||
print(error);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Avoid memory leak (`setState` after dispose) and disconnect
|
||||
if (isConnected) {
|
||||
isDisconnecting = true;
|
||||
connection.dispose();
|
||||
connection = null;
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Row> list = messages.map((_message) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
child: Text(
|
||||
(text) {
|
||||
return text == '/shrug' ? '¯\\_(ツ)_/¯' : text;
|
||||
}(_message.text.trim()),
|
||||
style: TextStyle(color: Colors.white)),
|
||||
padding: EdgeInsets.all(12.0),
|
||||
margin: EdgeInsets.only(bottom: 8.0, left: 8.0, right: 8.0),
|
||||
width: 222.0,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
_message.whom == clientID ? Colors.blueAccent : Colors.grey,
|
||||
borderRadius: BorderRadius.circular(7.0)),
|
||||
),
|
||||
],
|
||||
mainAxisAlignment: _message.whom == clientID
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: (isConnecting
|
||||
? Text('Connecting chat to ' + widget.server.name + '...')
|
||||
: isConnected
|
||||
? Text('Live chat with ' + widget.server.name)
|
||||
: Text('Chat log with ' + widget.server.name))),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
controller: listScrollController,
|
||||
children: list),
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: 16.0),
|
||||
child: TextField(
|
||||
style: const TextStyle(fontSize: 15.0),
|
||||
controller: textEditingController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: isConnecting
|
||||
? 'Wait until connected...'
|
||||
: isConnected
|
||||
? 'Type your message...'
|
||||
: 'Chat got disconnected',
|
||||
hintStyle: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
enabled: isConnected,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: isConnected
|
||||
? () => _sendMessage(textEditingController.text)
|
||||
: null),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDataReceived(Uint8List data) {
|
||||
// Allocate buffer for parsed data
|
||||
int backspacesCounter = 0;
|
||||
data.forEach((byte) {
|
||||
if (byte == 8 || byte == 127) {
|
||||
backspacesCounter++;
|
||||
}
|
||||
});
|
||||
Uint8List buffer = Uint8List(data.length - backspacesCounter);
|
||||
int bufferIndex = buffer.length;
|
||||
|
||||
// Apply backspace control character
|
||||
backspacesCounter = 0;
|
||||
for (int i = data.length - 1; i >= 0; i--) {
|
||||
if (data[i] == 8 || data[i] == 127) {
|
||||
backspacesCounter++;
|
||||
} else {
|
||||
if (backspacesCounter > 0) {
|
||||
backspacesCounter--;
|
||||
} else {
|
||||
buffer[--bufferIndex] = data[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create message if there is new line character
|
||||
String dataString = String.fromCharCodes(buffer);
|
||||
int index = buffer.indexOf(13);
|
||||
if (~index != 0) {
|
||||
setState(() {
|
||||
messages.add(
|
||||
_Message(
|
||||
1,
|
||||
backspacesCounter > 0
|
||||
? _messageBuffer.substring(
|
||||
0, _messageBuffer.length - backspacesCounter)
|
||||
: _messageBuffer + dataString.substring(0, index),
|
||||
),
|
||||
);
|
||||
_messageBuffer = dataString.substring(index);
|
||||
});
|
||||
} else {
|
||||
_messageBuffer = (backspacesCounter > 0
|
||||
? _messageBuffer.substring(
|
||||
0, _messageBuffer.length - backspacesCounter)
|
||||
: _messageBuffer + dataString);
|
||||
}
|
||||
}
|
||||
|
||||
void _sendMessage(String text) async {
|
||||
text = text.trim();
|
||||
textEditingController.clear();
|
||||
|
||||
if (text.length > 0) {
|
||||
try {
|
||||
connection.output.add(utf8.encode(text + "\r\n"));
|
||||
await connection.output.allSent;
|
||||
|
||||
setState(() {
|
||||
messages.add(_Message(clientID, text));
|
||||
});
|
||||
|
||||
Future.delayed(Duration(milliseconds: 333)).then((_) {
|
||||
listScrollController.animateTo(
|
||||
listScrollController.position.maxScrollExtent,
|
||||
duration: Duration(milliseconds: 333),
|
||||
curve: Curves.easeOut);
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore error, but notify state
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||
|
||||
import './BluetoothDeviceListEntry.dart';
|
||||
|
||||
class DiscoveryPage extends StatefulWidget {
|
||||
/// If true, discovery starts on page start, otherwise user must press action button.
|
||||
final bool start;
|
||||
|
||||
const DiscoveryPage({this.start = true});
|
||||
|
||||
@override
|
||||
_DiscoveryPage createState() => new _DiscoveryPage();
|
||||
}
|
||||
|
||||
class _DiscoveryPage extends State<DiscoveryPage> {
|
||||
StreamSubscription<BluetoothDiscoveryResult> _streamSubscription;
|
||||
List<BluetoothDiscoveryResult> results = List<BluetoothDiscoveryResult>();
|
||||
bool isDiscovering;
|
||||
|
||||
_DiscoveryPage();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
isDiscovering = widget.start;
|
||||
if (isDiscovering) {
|
||||
_startDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
void _restartDiscovery() {
|
||||
setState(() {
|
||||
results.clear();
|
||||
isDiscovering = true;
|
||||
});
|
||||
|
||||
_startDiscovery();
|
||||
}
|
||||
|
||||
void _startDiscovery() {
|
||||
_streamSubscription =
|
||||
FlutterBluetoothSerial.instance.startDiscovery().listen((r) {
|
||||
setState(() {
|
||||
results.add(r);
|
||||
});
|
||||
});
|
||||
|
||||
_streamSubscription.onDone(() {
|
||||
setState(() {
|
||||
isDiscovering = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// @TODO . One day there should be `_pairDevice` on long tap on something... ;)
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Avoid memory leak (`setState` after dispose) and cancel discovery
|
||||
_streamSubscription?.cancel();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: isDiscovering
|
||||
? Text('Discovering devices')
|
||||
: Text('Discovered devices'),
|
||||
actions: <Widget>[
|
||||
isDiscovering
|
||||
? FittedBox(
|
||||
child: Container(
|
||||
margin: new EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
icon: Icon(Icons.replay),
|
||||
onPressed: _restartDiscovery,
|
||||
)
|
||||
],
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemCount: results.length,
|
||||
itemBuilder: (BuildContext context, index) {
|
||||
BluetoothDiscoveryResult result = results[index];
|
||||
return BluetoothDeviceListEntry(
|
||||
device: result.device,
|
||||
rssi: result.rssi,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop(result.device);
|
||||
},
|
||||
onLongPress: () async {
|
||||
try {
|
||||
bool bonded = false;
|
||||
if (result.device.isBonded) {
|
||||
print('Unbonding from ${result.device.address}...');
|
||||
await FlutterBluetoothSerial.instance
|
||||
.removeDeviceBondWithAddress(result.device.address);
|
||||
print('Unbonding from ${result.device.address} has succed');
|
||||
} else {
|
||||
print('Bonding with ${result.device.address}...');
|
||||
bonded = await FlutterBluetoothSerial.instance
|
||||
.bondDeviceAtAddress(result.device.address);
|
||||
print(
|
||||
'Bonding with ${result.device.address} has ${bonded ? 'succed' : 'failed'}.');
|
||||
}
|
||||
setState(() {
|
||||
results[results.indexOf(result)] = BluetoothDiscoveryResult(
|
||||
device: BluetoothDevice(
|
||||
name: result.device.name ?? '',
|
||||
address: result.device.address,
|
||||
type: result.device.type,
|
||||
bondState: bonded
|
||||
? BluetoothBondState.bonded
|
||||
: BluetoothBondState.none,
|
||||
),
|
||||
rssi: result.rssi);
|
||||
});
|
||||
} catch (ex) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Error occured while bonding'),
|
||||
content: Text("${ex.toString()}"),
|
||||
actions: <Widget>[
|
||||
new FlatButton(
|
||||
child: new Text("Close"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||
import 'package:scoped_model/scoped_model.dart';
|
||||
|
||||
import './DiscoveryPage.dart';
|
||||
import './SelectBondedDevicePage.dart';
|
||||
import './ChatPage.dart';
|
||||
import './BackgroundCollectingTask.dart';
|
||||
import './BackgroundCollectedPage.dart';
|
||||
|
||||
// import './helpers/LineChart.dart';
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
@override
|
||||
_MainPage createState() => new _MainPage();
|
||||
}
|
||||
|
||||
class _MainPage extends State<MainPage> {
|
||||
BluetoothState _bluetoothState = BluetoothState.UNKNOWN;
|
||||
|
||||
String _address = "...";
|
||||
String _name = "...";
|
||||
|
||||
Timer _discoverableTimeoutTimer;
|
||||
int _discoverableTimeoutSecondsLeft = 0;
|
||||
|
||||
BackgroundCollectingTask _collectingTask;
|
||||
|
||||
bool _autoAcceptPairingRequests = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Get current state
|
||||
FlutterBluetoothSerial.instance.state.then((state) {
|
||||
setState(() {
|
||||
_bluetoothState = state;
|
||||
});
|
||||
});
|
||||
|
||||
Future.doWhile(() async {
|
||||
// Wait if adapter not enabled
|
||||
if (await FlutterBluetoothSerial.instance.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
await Future.delayed(Duration(milliseconds: 0xDD));
|
||||
return true;
|
||||
}).then((_) {
|
||||
// Update the address field
|
||||
FlutterBluetoothSerial.instance.address.then((address) {
|
||||
setState(() {
|
||||
_address = address;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
FlutterBluetoothSerial.instance.name.then((name) {
|
||||
setState(() {
|
||||
_name = name;
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for futher state changes
|
||||
FlutterBluetoothSerial.instance
|
||||
.onStateChanged()
|
||||
.listen((BluetoothState state) {
|
||||
setState(() {
|
||||
_bluetoothState = state;
|
||||
|
||||
// Discoverable mode is disabled when Bluetooth gets disabled
|
||||
_discoverableTimeoutTimer = null;
|
||||
_discoverableTimeoutSecondsLeft = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
FlutterBluetoothSerial.instance.setPairingRequestHandler(null);
|
||||
_collectingTask?.dispose();
|
||||
_discoverableTimeoutTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Flutter Bluetooth Serial'),
|
||||
),
|
||||
body: Container(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
ListTile(title: const Text('General')),
|
||||
SwitchListTile(
|
||||
title: const Text('Enable Bluetooth'),
|
||||
value: _bluetoothState.isEnabled,
|
||||
onChanged: (bool value) {
|
||||
// Do the request and update with the true value then
|
||||
future() async {
|
||||
// async lambda seems to not working
|
||||
if (value)
|
||||
await FlutterBluetoothSerial.instance.requestEnable();
|
||||
else
|
||||
await FlutterBluetoothSerial.instance.requestDisable();
|
||||
}
|
||||
|
||||
future().then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Bluetooth status'),
|
||||
subtitle: Text(_bluetoothState.toString()),
|
||||
trailing: RaisedButton(
|
||||
child: const Text('Settings'),
|
||||
onPressed: () {
|
||||
FlutterBluetoothSerial.instance.openSettings();
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Local adapter address'),
|
||||
subtitle: Text(_address),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Local adapter name'),
|
||||
subtitle: Text(_name),
|
||||
onLongPress: null,
|
||||
),
|
||||
ListTile(
|
||||
title: _discoverableTimeoutSecondsLeft == 0
|
||||
? const Text("Discoverable")
|
||||
: Text(
|
||||
"Discoverable for ${_discoverableTimeoutSecondsLeft}s"),
|
||||
subtitle: const Text("PsychoX-Luna"),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _discoverableTimeoutSecondsLeft != 0,
|
||||
onChanged: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () async {
|
||||
print('Discoverable requested');
|
||||
final int timeout = await FlutterBluetoothSerial.instance
|
||||
.requestDiscoverable(60);
|
||||
if (timeout < 0) {
|
||||
print('Discoverable mode denied');
|
||||
} else {
|
||||
print(
|
||||
'Discoverable mode acquired for $timeout seconds');
|
||||
}
|
||||
setState(() {
|
||||
_discoverableTimeoutTimer?.cancel();
|
||||
_discoverableTimeoutSecondsLeft = timeout;
|
||||
_discoverableTimeoutTimer =
|
||||
Timer.periodic(Duration(seconds: 1), (Timer timer) {
|
||||
setState(() {
|
||||
if (_discoverableTimeoutSecondsLeft < 0) {
|
||||
FlutterBluetoothSerial.instance.isDiscoverable
|
||||
.then((isDiscoverable) {
|
||||
if (isDiscoverable) {
|
||||
print(
|
||||
"Discoverable after timeout... might be infinity timeout :F");
|
||||
_discoverableTimeoutSecondsLeft += 1;
|
||||
}
|
||||
});
|
||||
timer.cancel();
|
||||
_discoverableTimeoutSecondsLeft = 0;
|
||||
} else {
|
||||
_discoverableTimeoutSecondsLeft -= 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
ListTile(title: const Text('Devices discovery and connection')),
|
||||
SwitchListTile(
|
||||
title: const Text('Auto-try specific pin when pairing'),
|
||||
subtitle: const Text('Pin 1234'),
|
||||
value: _autoAcceptPairingRequests,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_autoAcceptPairingRequests = value;
|
||||
});
|
||||
if (value) {
|
||||
FlutterBluetoothSerial.instance.setPairingRequestHandler(
|
||||
(BluetoothPairingRequest request) {
|
||||
print("Trying to auto-pair with Pin 1234");
|
||||
if (request.pairingVariant == PairingVariant.Pin) {
|
||||
return Future.value("1234");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
FlutterBluetoothSerial.instance
|
||||
.setPairingRequestHandler(null);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: RaisedButton(
|
||||
child: const Text('Explore discovered devices'),
|
||||
onPressed: () async {
|
||||
final BluetoothDevice selectedDevice =
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return DiscoveryPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedDevice != null) {
|
||||
print('Discovery -> selected ' + selectedDevice.address);
|
||||
} else {
|
||||
print('Discovery -> no device selected');
|
||||
}
|
||||
}),
|
||||
),
|
||||
ListTile(
|
||||
title: RaisedButton(
|
||||
child: const Text('Connect to paired device to chat'),
|
||||
onPressed: () async {
|
||||
final BluetoothDevice selectedDevice =
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return SelectBondedDevicePage(checkAvailability: false);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedDevice != null) {
|
||||
print('Connect -> selected ' + selectedDevice.address);
|
||||
_startChat(context, selectedDevice);
|
||||
} else {
|
||||
print('Connect -> no device selected');
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
ListTile(title: const Text('Multiple connections example')),
|
||||
ListTile(
|
||||
title: RaisedButton(
|
||||
child: ((_collectingTask != null && _collectingTask.inProgress)
|
||||
? const Text('Disconnect and stop background collecting')
|
||||
: const Text('Connect to start background collecting')),
|
||||
onPressed: () async {
|
||||
if (_collectingTask != null && _collectingTask.inProgress) {
|
||||
await _collectingTask.cancel();
|
||||
setState(() {
|
||||
/* Update for `_collectingTask.inProgress` */
|
||||
});
|
||||
} else {
|
||||
final BluetoothDevice selectedDevice =
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return SelectBondedDevicePage(
|
||||
checkAvailability: false);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedDevice != null) {
|
||||
await _startBackgroundTask(context, selectedDevice);
|
||||
setState(() {
|
||||
/* Update for `_collectingTask.inProgress` */
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: RaisedButton(
|
||||
child: const Text('View background collected data'),
|
||||
onPressed: (_collectingTask != null)
|
||||
? () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ScopedModel<BackgroundCollectingTask>(
|
||||
model: _collectingTask,
|
||||
child: BackgroundCollectedPage(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _startChat(BuildContext context, BluetoothDevice server) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ChatPage(server: server);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startBackgroundTask(
|
||||
BuildContext context,
|
||||
BluetoothDevice server,
|
||||
) async {
|
||||
try {
|
||||
_collectingTask = await BackgroundCollectingTask.connect(server);
|
||||
await _collectingTask.start();
|
||||
} catch (ex) {
|
||||
if (_collectingTask != null) {
|
||||
_collectingTask.cancel();
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Error occured while connecting'),
|
||||
content: Text("${ex.toString()}"),
|
||||
actions: <Widget>[
|
||||
new FlatButton(
|
||||
child: new Text("Close"),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||
|
||||
import './BluetoothDeviceListEntry.dart';
|
||||
|
||||
class SelectBondedDevicePage extends StatefulWidget {
|
||||
/// If true, on page start there is performed discovery upon the bonded devices.
|
||||
/// Then, if they are not avaliable, they would be disabled from the selection.
|
||||
final bool checkAvailability;
|
||||
|
||||
const SelectBondedDevicePage({this.checkAvailability = true});
|
||||
|
||||
@override
|
||||
_SelectBondedDevicePage createState() => new _SelectBondedDevicePage();
|
||||
}
|
||||
|
||||
enum _DeviceAvailability {
|
||||
no,
|
||||
maybe,
|
||||
yes,
|
||||
}
|
||||
|
||||
class _DeviceWithAvailability extends BluetoothDevice {
|
||||
BluetoothDevice device;
|
||||
_DeviceAvailability availability;
|
||||
int rssi;
|
||||
|
||||
_DeviceWithAvailability(this.device, this.availability, [this.rssi]);
|
||||
}
|
||||
|
||||
class _SelectBondedDevicePage extends State<SelectBondedDevicePage> {
|
||||
List<_DeviceWithAvailability> devices = List<_DeviceWithAvailability>();
|
||||
|
||||
// Availability
|
||||
StreamSubscription<BluetoothDiscoveryResult> _discoveryStreamSubscription;
|
||||
bool _isDiscovering;
|
||||
|
||||
_SelectBondedDevicePage();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_isDiscovering = widget.checkAvailability;
|
||||
|
||||
if (_isDiscovering) {
|
||||
_startDiscovery();
|
||||
}
|
||||
|
||||
// Setup a list of the bonded devices
|
||||
FlutterBluetoothSerial.instance
|
||||
.getBondedDevices()
|
||||
.then((List<BluetoothDevice> bondedDevices) {
|
||||
setState(() {
|
||||
devices = bondedDevices
|
||||
.map(
|
||||
(device) => _DeviceWithAvailability(
|
||||
device,
|
||||
widget.checkAvailability
|
||||
? _DeviceAvailability.maybe
|
||||
: _DeviceAvailability.yes,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _restartDiscovery() {
|
||||
setState(() {
|
||||
_isDiscovering = true;
|
||||
});
|
||||
|
||||
_startDiscovery();
|
||||
}
|
||||
|
||||
void _startDiscovery() {
|
||||
_discoveryStreamSubscription =
|
||||
FlutterBluetoothSerial.instance.startDiscovery().listen((r) {
|
||||
setState(() {
|
||||
Iterator i = devices.iterator;
|
||||
while (i.moveNext()) {
|
||||
var _device = i.current;
|
||||
if (_device.device == r.device) {
|
||||
_device.availability = _DeviceAvailability.yes;
|
||||
_device.rssi = r.rssi;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
_discoveryStreamSubscription.onDone(() {
|
||||
setState(() {
|
||||
_isDiscovering = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Avoid memory leak (`setState` after dispose) and cancel discovery
|
||||
_discoveryStreamSubscription?.cancel();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<BluetoothDeviceListEntry> list = devices
|
||||
.map((_device) => BluetoothDeviceListEntry(
|
||||
device: _device.device,
|
||||
rssi: _device.rssi,
|
||||
enabled: _device.availability == _DeviceAvailability.yes,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop(_device.device);
|
||||
},
|
||||
))
|
||||
.toList();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Select device'),
|
||||
actions: <Widget>[
|
||||
_isDiscovering
|
||||
? FittedBox(
|
||||
child: Container(
|
||||
margin: new EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
icon: Icon(Icons.replay),
|
||||
onPressed: _restartDiscovery,
|
||||
)
|
||||
],
|
||||
),
|
||||
body: ListView(children: list),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
/// @name LineChart
|
||||
/// @version 0.0.5
|
||||
/// @description Simple line chart widget
|
||||
/// @author Patryk "PsychoX" Ludwikowski <patryk.ludwikowski.7+dart@gmail.com>
|
||||
/// @license MIT License (see https://mit-license.org/)
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:math' as math show min, max;
|
||||
|
||||
import './PaintStyle.dart';
|
||||
|
||||
class LabelEntry {
|
||||
final double value;
|
||||
final String label;
|
||||
|
||||
LabelEntry(this.value, this.label);
|
||||
}
|
||||
|
||||
/// Widget that allows to show data on line chart.
|
||||
///
|
||||
/// All arguments, values and labels data should be sorted!
|
||||
/// Since both the arguments and the values must be `double` type,
|
||||
/// be aware of the precision.
|
||||
class LineChart extends StatelessWidget {
|
||||
/// Constraints for the line chart.
|
||||
final BoxConstraints constraints;
|
||||
|
||||
// @TODO ? Both `_LineChartPainter` and `LineChart` have most the same fields.
|
||||
// `LineChart` is just mainly passing them to the painter. Shouldn't there be
|
||||
// only one class containing these data? Some `LineChartData` forged inside here
|
||||
// and then passed and used by the painter? :thinking:
|
||||
|
||||
/// Padding around main drawng area. Necessary for displaying labels (around the chart).
|
||||
final EdgeInsets padding;
|
||||
|
||||
/* Arguments */
|
||||
/// Collection of doubles as arguments.
|
||||
final Iterable<double> arguments;
|
||||
|
||||
/// Mappings of strings for doubles arguments, which allow to specify custom
|
||||
/// strings as labels for certain arguments.
|
||||
final Iterable<LabelEntry> argumentsLabels;
|
||||
|
||||
/* Values */
|
||||
/// Collection of data series as collections of next values on corresponding arguments.
|
||||
final Iterable<Iterable<double>> values;
|
||||
|
||||
/// Mappings of string for doubles values, which allow to specify custom
|
||||
/// string as labels for certain values.
|
||||
final Iterable<LabelEntry> valuesLabels;
|
||||
|
||||
/* Labels & lines styles */
|
||||
/// Style of horizontal lines labels
|
||||
final TextStyle horizontalLabelsTextStyle;
|
||||
|
||||
/// Style of vertical lines labels
|
||||
final TextStyle verticalLabelsTextStyle;
|
||||
|
||||
/// Defines style of horizontal lines. Might be null in order to prevent lines from drawing.
|
||||
final Paint horizontalLinesPaint;
|
||||
|
||||
/// Defines style of vertical lines. Might be null in order to prevent lines from drawing.
|
||||
final Paint verticalLinesPaint;
|
||||
|
||||
// @TODO . expose it
|
||||
final bool snapToLeftLabel = false;
|
||||
final bool snapToTopLabel = true;
|
||||
final bool snapToRightLabel = false;
|
||||
final bool snapToBottomLabel = true;
|
||||
|
||||
/* Series points & lines styles */
|
||||
/// List of paint styles for series values points.
|
||||
///
|
||||
/// On whole list null would use predefined set of styles.
|
||||
/// On list entry null there will be no points for certain series.
|
||||
final List<Paint> seriesPointsPaints;
|
||||
|
||||
/// List of paint styles for lines between next series points.
|
||||
///
|
||||
/// On null there will be no lines.
|
||||
final List<Paint> seriesLinesPaints;
|
||||
|
||||
final double additionalMinimalHorizontalLabelsInterval;
|
||||
final double additionalMinimalVerticalLablesInterval;
|
||||
|
||||
LineChart({
|
||||
@required this.constraints,
|
||||
this.padding = const EdgeInsets.fromLTRB(32, 12, 20, 28),
|
||||
this.arguments,
|
||||
this.argumentsLabels,
|
||||
this.values,
|
||||
this.valuesLabels,
|
||||
this.horizontalLabelsTextStyle,
|
||||
this.verticalLabelsTextStyle,
|
||||
PaintStyle horizontalLinesStyle = const PaintStyle(color: Colors.grey),
|
||||
PaintStyle verticalLinesStyle, // null for default
|
||||
|
||||
this.additionalMinimalHorizontalLabelsInterval = 8,
|
||||
this.additionalMinimalVerticalLablesInterval = 8,
|
||||
Iterable<PaintStyle>
|
||||
seriesPointsStyles, // null would use predefined set of styles
|
||||
Iterable<PaintStyle> seriesLinesStyles, // null for default
|
||||
}) : horizontalLinesPaint = horizontalLinesStyle?.toPaint(),
|
||||
verticalLinesPaint = verticalLinesStyle?.toPaint(),
|
||||
seriesPointsPaints = _prepareSeriesPointsPaints(seriesPointsStyles),
|
||||
seriesLinesPaints = _prepareSeriesLinesPaints(seriesLinesStyles) {
|
||||
if (seriesPointsStyles.length < values.length &&
|
||||
12 /* default paints */ < values.length) {
|
||||
throw "Too few `seriesPointsPaintStyle`s! Try define more or limit number of displayed series";
|
||||
}
|
||||
if (seriesLinesStyles.length < values.length) {
|
||||
throw "Too few `seriesLinesStyles`s! Try define more or limit number of displayed series";
|
||||
}
|
||||
}
|
||||
|
||||
static Iterable<Paint> _prepareSeriesPointsPaints(
|
||||
Iterable<PaintStyle> seriesPointsStyles) {
|
||||
if (seriesPointsStyles == null) {
|
||||
// Default paint for points
|
||||
return List<Paint>.unmodifiable(<Paint>[
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.blue).toPaint(),
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.red).toPaint(),
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.yellow).toPaint(),
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.green).toPaint(),
|
||||
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.purple).toPaint(),
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.deepOrange).toPaint(),
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.brown).toPaint(),
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.lime).toPaint(),
|
||||
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.indigo).toPaint(),
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.pink).toPaint(),
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.amber).toPaint(),
|
||||
PaintStyle(strokeWidth: 1.7, color: Colors.teal).toPaint(),
|
||||
|
||||
// For more, user should specify them :F
|
||||
]);
|
||||
} else {
|
||||
return seriesPointsStyles.map((style) => style?.toPaint()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
static Iterable<Paint> _prepareSeriesLinesPaints(
|
||||
Iterable<PaintStyle> seriesLinesStyles) {
|
||||
if (seriesLinesStyles == null) {
|
||||
return null;
|
||||
} else {
|
||||
return seriesLinesStyles.map((style) => style.toPaint()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: this.constraints,
|
||||
child: CustomPaint(
|
||||
painter: _LineChartPainter(
|
||||
padding: padding,
|
||||
arguments: arguments,
|
||||
argumentsLabels: argumentsLabels,
|
||||
values: values,
|
||||
valuesLabels: valuesLabels,
|
||||
horizontalLabelsTextStyle:
|
||||
horizontalLabelsTextStyle ?? Theme.of(context).textTheme.caption,
|
||||
verticalLabelsTextStyle:
|
||||
verticalLabelsTextStyle ?? Theme.of(context).textTheme.caption,
|
||||
horizontalLinesPaint: horizontalLinesPaint,
|
||||
verticalLinesPaint: verticalLinesPaint,
|
||||
additionalMinimalHorizontalLabelsInterval:
|
||||
additionalMinimalHorizontalLabelsInterval,
|
||||
additionalMinimalVerticalLablesInterval:
|
||||
additionalMinimalVerticalLablesInterval,
|
||||
seriesPointsPaints: seriesPointsPaints,
|
||||
seriesLinesPaints: seriesLinesPaints,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
class _LineChartPainter extends CustomPainter {
|
||||
/// Padding around main drawng area. Necessary for displaying labels (around the chart).
|
||||
final EdgeInsets padding;
|
||||
|
||||
/* Arguments */
|
||||
/// Collection of doubles as arguments.
|
||||
final Iterable<double> arguments;
|
||||
|
||||
/// Mappings of strings for doubles arguments, which allow to specify custom
|
||||
/// strings as labels for certain arguments.
|
||||
final Iterable<LabelEntry> argumentsLabels;
|
||||
|
||||
/* Values */
|
||||
/// Collection of data series as collections of next values on corresponding arguments.
|
||||
final Iterable<Iterable<double>> values;
|
||||
|
||||
/// Mappings of string for doubles values, which allow to specify custom
|
||||
/// string as labels for certain values.
|
||||
final Iterable<LabelEntry> valuesLabels;
|
||||
|
||||
/* Labels & lines styles */
|
||||
/// Style of horizontal lines labels
|
||||
final TextStyle horizontalLabelsTextStyle;
|
||||
|
||||
/// Style of vertical lines labels
|
||||
final TextStyle verticalLabelsTextStyle;
|
||||
|
||||
/// Defines style of horizontal lines. Might be null in order to prevent lines from drawing.
|
||||
final Paint horizontalLinesPaint;
|
||||
|
||||
/// Defines style of vertical lines. Might be null in order to prevent lines from drawing.
|
||||
final Paint verticalLinesPaint;
|
||||
|
||||
// @TODO . expose it
|
||||
final bool snapToLeftLabel = false;
|
||||
final bool snapToTopLabel = true;
|
||||
final bool snapToRightLabel = false;
|
||||
final bool snapToBottomLabel = true;
|
||||
|
||||
/* Series points & lines styles */
|
||||
/// Collection of paint styles for series values points.
|
||||
///
|
||||
/// On whole argument null would use predefined set of styles.
|
||||
/// On collection entry null there will be no points for certain series.
|
||||
final Iterable<Paint> seriesPointsPaints;
|
||||
|
||||
/// Collection of paint styles for lines between next series points.
|
||||
///
|
||||
/// On null there will be no lines.
|
||||
final Iterable<Paint> seriesLinesPaints;
|
||||
|
||||
/* Runtime */
|
||||
/// Minimal allowed interval between horizontal lines. Calculated from font size.
|
||||
final double minimalHorizontalLabelsInterval;
|
||||
|
||||
/// Maximal value of all data series values
|
||||
double maxValue = -double.maxFinite;
|
||||
|
||||
/// Minimal value of all data series values
|
||||
double minValue = double.maxFinite;
|
||||
|
||||
double _minimalHorizontalRatio = 0;
|
||||
double _minimalVerticalRatio = 0;
|
||||
|
||||
/// Creates `_LineChartPainter` (`CustomPainter`) with given data and styling.
|
||||
_LineChartPainter({
|
||||
this.padding = const EdgeInsets.fromLTRB(40, 8, 8, 32),
|
||||
this.arguments,
|
||||
this.argumentsLabels,
|
||||
this.values,
|
||||
this.valuesLabels,
|
||||
@required this.horizontalLabelsTextStyle,
|
||||
@required this.verticalLabelsTextStyle,
|
||||
@required this.horizontalLinesPaint,
|
||||
@required this.verticalLinesPaint,
|
||||
double additionalMinimalHorizontalLabelsInterval = 8,
|
||||
double additionalMinimalVerticalLablesInterval = 8,
|
||||
@required this.seriesPointsPaints,
|
||||
@required this.seriesLinesPaints,
|
||||
}) : this.minimalHorizontalLabelsInterval =
|
||||
horizontalLabelsTextStyle.fontSize +
|
||||
additionalMinimalHorizontalLabelsInterval {
|
||||
// Find max & min values of data to be show
|
||||
for (Iterable<double> series in values) {
|
||||
for (double value in series) {
|
||||
if (value > maxValue) {
|
||||
maxValue = value;
|
||||
} else if (value < minValue) {
|
||||
minValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (valuesLabels != null) {
|
||||
// Find minimal vertical ratio to fit all provided values labels
|
||||
Iterator<LabelEntry> entry = valuesLabels.iterator;
|
||||
entry.moveNext();
|
||||
double lastValue = entry.current.value;
|
||||
|
||||
while (entry.moveNext()) {
|
||||
final double goodRatio =
|
||||
minimalHorizontalLabelsInterval / (entry.current.value - lastValue);
|
||||
if (goodRatio > _minimalVerticalRatio) {
|
||||
_minimalVerticalRatio = goodRatio;
|
||||
}
|
||||
|
||||
lastValue = entry.current.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (argumentsLabels != null) {
|
||||
// Find minimal horizontal ratio to fit all provided arguments labels
|
||||
Iterator<LabelEntry> entry = argumentsLabels.iterator;
|
||||
entry.moveNext();
|
||||
double lastValue = entry.current.value;
|
||||
double lastWidth =
|
||||
_getLabelTextPainter(entry.current.label, verticalLabelsTextStyle)
|
||||
.width;
|
||||
|
||||
while (entry.moveNext()) {
|
||||
final double nextValue = entry.current.value;
|
||||
final double nextWidth =
|
||||
_getLabelTextPainter(entry.current.label, verticalLabelsTextStyle)
|
||||
.width;
|
||||
|
||||
final double goodRatio = ((lastWidth + nextWidth) / 2 +
|
||||
additionalMinimalVerticalLablesInterval) /
|
||||
(nextValue - lastValue);
|
||||
if (goodRatio > _minimalHorizontalRatio) {
|
||||
_minimalHorizontalRatio = goodRatio;
|
||||
}
|
||||
|
||||
lastValue = nextValue;
|
||||
lastWidth = nextWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final double width = size.width - padding.left - padding.right;
|
||||
final double height = size.height - padding.top - padding.bottom;
|
||||
|
||||
/* Horizontal lines with labels */
|
||||
double valuesOffset = 0; // @TODO ? could be used in future for scrolling
|
||||
double verticalRatio;
|
||||
|
||||
{
|
||||
Iterable<LabelEntry> labels;
|
||||
|
||||
// If no labels provided - generate them!
|
||||
if (valuesLabels == null) {
|
||||
final double optimalStepValue =
|
||||
_calculateOptimalStepValue(maxValue - minValue, height);
|
||||
int stepsNumber = 1;
|
||||
|
||||
// Find bottom line value
|
||||
double bottomValue = 0;
|
||||
if (minValue > 0) {
|
||||
while (bottomValue < minValue) {
|
||||
bottomValue += optimalStepValue;
|
||||
}
|
||||
bottomValue -= optimalStepValue;
|
||||
} else {
|
||||
while (bottomValue > minValue) {
|
||||
bottomValue -= optimalStepValue;
|
||||
}
|
||||
}
|
||||
valuesOffset = bottomValue;
|
||||
|
||||
// Find top line value
|
||||
double topValue = bottomValue;
|
||||
while (topValue < maxValue) {
|
||||
topValue += optimalStepValue;
|
||||
stepsNumber += 1;
|
||||
}
|
||||
|
||||
// Set labels iterable from prepared generator
|
||||
Iterable<LabelEntry> generator(double optimalStepValue, int stepsNumber,
|
||||
[double value = 0.0]) sync* {
|
||||
//double value = _bottomValue;
|
||||
for (int i = 0; i < stepsNumber; i++) {
|
||||
yield LabelEntry(
|
||||
value,
|
||||
value
|
||||
.toString()); // @TODO , choose better precision based on optimal step value while parsing to string
|
||||
value += optimalStepValue;
|
||||
}
|
||||
}
|
||||
|
||||
labels = generator(optimalStepValue, stepsNumber, bottomValue);
|
||||
|
||||
if (!snapToTopLabel) {
|
||||
topValue = maxValue;
|
||||
}
|
||||
if (!snapToBottomLabel) {
|
||||
bottomValue = valuesOffset = minValue;
|
||||
}
|
||||
|
||||
// Calculate vertical ratio of pixels per value
|
||||
// Note: There is no empty space already
|
||||
verticalRatio = height / (topValue - bottomValue);
|
||||
}
|
||||
// If labels provided - use them
|
||||
else {
|
||||
// Set labels iterable as the provided list
|
||||
labels = valuesLabels;
|
||||
|
||||
// Use minimal visible value as offset.
|
||||
// Note: `minValue` is calculated in constructor and includes miniaml labels values.
|
||||
valuesOffset = minValue;
|
||||
|
||||
// Calculate vertical ratio of pixels per value
|
||||
// Note: `_minimalVerticalRatio` is calculated in constructor
|
||||
final double topValue = snapToTopLabel
|
||||
? math.max(maxValue, valuesLabels.last.value)
|
||||
: maxValue;
|
||||
final double bottomValue = snapToBottomLabel
|
||||
? math.min(minValue, valuesLabels.first.value)
|
||||
: minValue;
|
||||
final double noEmptySpaceRatio = height / (topValue - bottomValue);
|
||||
verticalRatio = math.max(_minimalVerticalRatio, noEmptySpaceRatio);
|
||||
}
|
||||
|
||||
// Draw the horizontal lines and labels
|
||||
for (LabelEntry tuple in labels) {
|
||||
if (tuple.value < valuesOffset) continue;
|
||||
final double yOffset = (size.height -
|
||||
padding.bottom -
|
||||
(tuple.value - valuesOffset) * verticalRatio);
|
||||
if (yOffset < padding.top) break;
|
||||
|
||||
// Draw line
|
||||
if (horizontalLinesPaint != null) {
|
||||
canvas.drawLine(
|
||||
Offset(padding.left, yOffset),
|
||||
Offset(size.width - padding.right, yOffset),
|
||||
horizontalLinesPaint);
|
||||
}
|
||||
|
||||
// Draw label
|
||||
TextPainter(
|
||||
text: TextSpan(text: tuple.label, style: horizontalLabelsTextStyle),
|
||||
textAlign: TextAlign.right,
|
||||
textDirection: TextDirection.ltr)
|
||||
..layout(minWidth: padding.left - 4)
|
||||
..paint(canvas,
|
||||
Offset(0, yOffset - horizontalLabelsTextStyle.fontSize / 2 - 1));
|
||||
}
|
||||
}
|
||||
|
||||
/* Vertical lines with labels */
|
||||
double argumentsOffset = 0;
|
||||
final double xOffsetLimit = size.width - padding.right;
|
||||
double horizontalRatio;
|
||||
|
||||
{
|
||||
Iterable<LabelEntry> labels;
|
||||
|
||||
// If no labels provided - generate them!
|
||||
if (argumentsLabels == null) {
|
||||
throw "not implemented";
|
||||
// @TODO . after few hot days of thinking about the problem for 1-2 hour a day, I just gave up.
|
||||
// The hardest in the problem is that there must be trade-off between space for labels and max lines,
|
||||
// but keep in mind that the label values should be in some human-readable steps (0.5, 10, 0.02...).
|
||||
}
|
||||
// If labels provided - use them
|
||||
else {
|
||||
// Set labels iterable as the provided list
|
||||
labels = argumentsLabels;
|
||||
|
||||
// Use first visible argument as arguments offset
|
||||
argumentsOffset = argumentsLabels.first.value;
|
||||
|
||||
if (!snapToLeftLabel) {
|
||||
argumentsOffset = arguments.first;
|
||||
}
|
||||
|
||||
// Calculate vertical ratio of pixels per value
|
||||
// Note: `_minimalHorizontalRatio` is calculated in constructor
|
||||
final double leftMost = snapToLeftLabel
|
||||
? math.min(arguments.first, argumentsLabels.first.value)
|
||||
: arguments.first;
|
||||
final double rightMost = snapToRightLabel
|
||||
? math.max(arguments.last, argumentsLabels.last.value)
|
||||
: arguments.last;
|
||||
final double noEmptySpaceRatio = width / (rightMost - leftMost);
|
||||
horizontalRatio = math.max(_minimalHorizontalRatio, noEmptySpaceRatio);
|
||||
}
|
||||
|
||||
// Draw the vertical lines and labels
|
||||
for (LabelEntry tuple in labels) {
|
||||
if (tuple.value < argumentsOffset) continue;
|
||||
final double xOffset =
|
||||
padding.left + (tuple.value - argumentsOffset) * horizontalRatio;
|
||||
if (xOffset > xOffsetLimit) break;
|
||||
|
||||
// Draw line
|
||||
if (verticalLinesPaint != null) {
|
||||
canvas.drawLine(
|
||||
Offset(xOffset, padding.top),
|
||||
Offset(xOffset, size.height - padding.bottom),
|
||||
verticalLinesPaint);
|
||||
}
|
||||
|
||||
// Draw label
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(text: tuple.label, style: verticalLabelsTextStyle),
|
||||
textDirection: TextDirection.ltr)
|
||||
..layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(xOffset - textPainter.width / 2,
|
||||
size.height - verticalLabelsTextStyle.fontSize - 8));
|
||||
}
|
||||
}
|
||||
|
||||
/* Points and lines between subsequent */
|
||||
Iterator<Iterable<double>> series = values.iterator;
|
||||
Iterator<Paint> linesPaints = seriesLinesPaints == null
|
||||
? <Paint>[].iterator
|
||||
: seriesLinesPaints.iterator;
|
||||
Iterator<Paint> pointsPaints = seriesPointsPaints.iterator;
|
||||
while (series.moveNext()) {
|
||||
List<Offset> points = [];
|
||||
Iterator<double> value = series.current.iterator;
|
||||
Iterator<double> argument = arguments.iterator;
|
||||
while (value.moveNext()) {
|
||||
argument.moveNext();
|
||||
if (value.current == null || value.current == double.nan) continue;
|
||||
|
||||
if (argument.current < argumentsOffset) continue;
|
||||
final double xOffset = padding.left +
|
||||
(argument.current - argumentsOffset) * horizontalRatio;
|
||||
if (xOffset > xOffsetLimit) break;
|
||||
|
||||
if (value.current < valuesOffset) continue;
|
||||
final double yOffset = size.height -
|
||||
padding.bottom -
|
||||
(value.current - valuesOffset) * verticalRatio;
|
||||
if (yOffset < padding.top) continue;
|
||||
|
||||
points.add(Offset(xOffset, yOffset));
|
||||
}
|
||||
|
||||
// Lines
|
||||
if (linesPaints.moveNext() && linesPaints.current != null) {
|
||||
canvas.drawPath(Path()..addPolygon(points, false), linesPaints.current);
|
||||
}
|
||||
|
||||
// Points
|
||||
if (pointsPaints.moveNext() && pointsPaints.current != null) {
|
||||
canvas.drawPoints(ui.PointMode.points, points, pointsPaints.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_LineChartPainter old) =>
|
||||
(this.arguments != old.arguments ||
|
||||
this.values != old.values ||
|
||||
this.argumentsLabels != old.argumentsLabels ||
|
||||
this.valuesLabels != old.valuesLabels ||
|
||||
this.seriesPointsPaints != old.seriesPointsPaints ||
|
||||
this.seriesLinesPaints != old.seriesLinesPaints ||
|
||||
this.horizontalLabelsTextStyle != old.horizontalLabelsTextStyle ||
|
||||
this.verticalLabelsTextStyle != old.verticalLabelsTextStyle ||
|
||||
this.padding != old.padding //
|
||||
);
|
||||
|
||||
// ..., 0.01, 0.02, 0.05, 0.1, [0.125], 0.2, [0.25], 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, ...
|
||||
double _calculateOptimalStepValue(double valueRange, double height) {
|
||||
final int maxSteps = height ~/ minimalHorizontalLabelsInterval;
|
||||
if (maxSteps <= 0) {
|
||||
throw "invalid max lines!";
|
||||
}
|
||||
double interval = valueRange / maxSteps;
|
||||
if (interval > 1) {
|
||||
int zeros = 0;
|
||||
while (interval >= 10) {
|
||||
interval = interval / 10;
|
||||
zeros += 1;
|
||||
}
|
||||
/**/ if (interval <= 1) {
|
||||
interval = 1;
|
||||
} else if (interval <= 2) {
|
||||
interval = 2;
|
||||
} else if (interval <= 5) {
|
||||
interval = 5;
|
||||
}
|
||||
for (; zeros-- != 0;) {
|
||||
interval *= 10;
|
||||
}
|
||||
} else {
|
||||
// @TODO ! not working at all for lower :C
|
||||
int zeros = 0;
|
||||
while (interval < 0) {
|
||||
interval = interval * 10;
|
||||
zeros += 1;
|
||||
}
|
||||
/**/ if (interval <= 1) {
|
||||
interval = 1;
|
||||
} else if (interval <= 2) {
|
||||
interval = 2;
|
||||
} else if (interval <= 5) {
|
||||
interval = 5;
|
||||
}
|
||||
for (; zeros-- != 0;) {
|
||||
interval /= 10;
|
||||
}
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
|
||||
TextPainter _getLabelTextPainter(String text, TextStyle style) {
|
||||
return TextPainter(
|
||||
text: TextSpan(text: text, style: style),
|
||||
textDirection: TextDirection.ltr)
|
||||
..layout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import 'dart:ui';
|
||||
|
||||
/// A description of the style to use when drawing on a [Canvas].
|
||||
///
|
||||
/// Most APIs on [Canvas] take a [Paint] object to describe the style
|
||||
/// to use for that operation. [PaintStyle] allows to be const
|
||||
/// constructed and later in runtime forged into the [Paint] object.
|
||||
class PaintStyle {
|
||||
/// Whether to apply anti-aliasing to lines and images drawn on the
|
||||
/// canvas.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool isAntiAlias;
|
||||
|
||||
// Must be kept in sync with the default in paint.cc.
|
||||
static const int _kColorDefault = 0xFF000000;
|
||||
|
||||
/// The color to use when stroking or filling a shape.
|
||||
///
|
||||
/// Defaults to opaque black.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [style], which controls whether to stroke or fill (or both).
|
||||
/// * [colorFilter], which overrides [color].
|
||||
/// * [shader], which overrides [color] with more elaborate effects.
|
||||
///
|
||||
/// This color is not used when compositing. To colorize a layer, use
|
||||
/// [colorFilter].
|
||||
final Color color;
|
||||
|
||||
// Must be kept in sync with the default in paint.cc.
|
||||
static final int _kBlendModeDefault = BlendMode.srcOver.index;
|
||||
|
||||
/// A blend mode to apply when a shape is drawn or a layer is composited.
|
||||
///
|
||||
/// The source colors are from the shape being drawn (e.g. from
|
||||
/// [Canvas.drawPath]) or layer being composited (the graphics that were drawn
|
||||
/// between the [Canvas.saveLayer] and [Canvas.restore] calls), after applying
|
||||
/// the [colorFilter], if any.
|
||||
///
|
||||
/// The destination colors are from the background onto which the shape or
|
||||
/// layer is being composited.
|
||||
///
|
||||
/// Defaults to [BlendMode.srcOver].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite
|
||||
/// the layer when [restore] is called.
|
||||
/// * [BlendMode], which discusses the user of [saveLayer] with [blendMode].
|
||||
final BlendMode blendMode;
|
||||
|
||||
/// Whether to paint inside shapes, the edges of shapes, or both.
|
||||
///
|
||||
/// Defaults to [PaintingStyle.fill].
|
||||
final PaintingStyle style;
|
||||
|
||||
/// How wide to make edges drawn when [style] is set to
|
||||
/// [PaintingStyle.stroke]. The width is given in logical pixels measured in
|
||||
/// the direction orthogonal to the direction of the path.
|
||||
///
|
||||
/// Defaults to 0.0, which correspond to a hairline width.
|
||||
final double strokeWidth;
|
||||
|
||||
/// The kind of finish to place on the end of lines drawn when
|
||||
/// [style] is set to [PaintingStyle.stroke].
|
||||
///
|
||||
/// Defaults to [StrokeCap.butt], i.e. no caps.
|
||||
final StrokeCap strokeCap;
|
||||
|
||||
/// The kind of finish to place on the joins between segments.
|
||||
///
|
||||
/// This applies to paths drawn when [style] is set to [PaintingStyle.stroke],
|
||||
/// It does not apply to points drawn as lines with [Canvas.drawPoints].
|
||||
///
|
||||
/// Defaults to [StrokeJoin.miter], i.e. sharp corners.
|
||||
///
|
||||
/// Some examples of joins:
|
||||
///
|
||||
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4}
|
||||
///
|
||||
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/round_join.mp4}
|
||||
///
|
||||
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/bevel_join.mp4}
|
||||
///
|
||||
/// The centers of the line segments are colored in the diagrams above to
|
||||
/// highlight the joins, but in normal usage the join is the same color as the
|
||||
/// line.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [strokeMiterLimit] to control when miters are replaced by bevels when
|
||||
/// this is set to [StrokeJoin.miter].
|
||||
/// * [strokeCap] to control what is drawn at the ends of the stroke.
|
||||
/// * [StrokeJoin] for the definitive list of stroke joins.
|
||||
final StrokeJoin strokeJoin;
|
||||
|
||||
// Must be kept in sync with the default in paint.cc.
|
||||
static const double _kStrokeMiterLimitDefault = 4.0;
|
||||
|
||||
/// The limit for miters to be drawn on segments when the join is set to
|
||||
/// [StrokeJoin.miter] and the [style] is set to [PaintingStyle.stroke]. If
|
||||
/// this limit is exceeded, then a [StrokeJoin.bevel] join will be drawn
|
||||
/// instead. This may cause some 'popping' of the corners of a path if the
|
||||
/// angle between line segments is animated, as seen in the diagrams below.
|
||||
///
|
||||
/// This limit is expressed as a limit on the length of the miter.
|
||||
///
|
||||
/// Defaults to 4.0. Using zero as a limit will cause a [StrokeJoin.bevel]
|
||||
/// join to be used all the time.
|
||||
///
|
||||
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_0_join.mp4}
|
||||
///
|
||||
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4}
|
||||
///
|
||||
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_6_join.mp4}
|
||||
///
|
||||
/// The centers of the line segments are colored in the diagrams above to
|
||||
/// highlight the joins, but in normal usage the join is the same color as the
|
||||
/// line.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [strokeJoin] to control the kind of finish to place on the joins
|
||||
/// between segments.
|
||||
/// * [strokeCap] to control what is drawn at the ends of the stroke.
|
||||
final double strokeMiterLimit;
|
||||
|
||||
/// A mask filter (for example, a blur) to apply to a shape after it has been
|
||||
/// drawn but before it has been composited into the image.
|
||||
///
|
||||
/// See [MaskFilter] for details.
|
||||
final MaskFilter maskFilter;
|
||||
|
||||
/// Controls the performance vs quality trade-off to use when applying
|
||||
/// filters, such as [maskFilter], or when drawing images, as with
|
||||
/// [Canvas.drawImageRect] or [Canvas.drawImageNine].
|
||||
///
|
||||
/// Defaults to [FilterQuality.none].
|
||||
// TODO(ianh): verify that the image drawing methods actually respect this
|
||||
final FilterQuality filterQuality;
|
||||
|
||||
/// The shader to use when stroking or filling a shape.
|
||||
///
|
||||
/// When this is null, the [color] is used instead.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Gradient], a shader that paints a color gradient.
|
||||
/// * [ImageShader], a shader that tiles an [Image].
|
||||
/// * [colorFilter], which overrides [shader].
|
||||
/// * [color], which is used if [shader] and [colorFilter] are null.
|
||||
final Shader shader;
|
||||
|
||||
/// A color filter to apply when a shape is drawn or when a layer is
|
||||
/// composited.
|
||||
///
|
||||
/// See [ColorFilter] for details.
|
||||
///
|
||||
/// When a shape is being drawn, [colorFilter] overrides [color] and [shader].
|
||||
final ColorFilter colorFilter;
|
||||
|
||||
/// Whether the colors of the image are inverted when drawn.
|
||||
///
|
||||
/// Inverting the colors of an image applies a new color filter that will
|
||||
/// be composed with any user provided color filters. This is primarily
|
||||
/// used for implementing smart invert on iOS.
|
||||
final bool invertColors;
|
||||
|
||||
const PaintStyle({
|
||||
this.isAntiAlias = true,
|
||||
this.color = const Color(_kColorDefault),
|
||||
this.blendMode = BlendMode.srcOver,
|
||||
this.style = PaintingStyle.fill,
|
||||
this.strokeWidth = 0.0,
|
||||
this.strokeCap = StrokeCap.butt,
|
||||
this.strokeJoin = StrokeJoin.miter,
|
||||
this.strokeMiterLimit = 4.0,
|
||||
this.maskFilter, // null
|
||||
this.filterQuality = FilterQuality.none,
|
||||
this.shader, // null
|
||||
this.colorFilter, // null
|
||||
this.invertColors = false,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final StringBuffer result = StringBuffer();
|
||||
String semicolon = '';
|
||||
result.write('PaintStyle(');
|
||||
if (style == PaintingStyle.stroke) {
|
||||
result.write('$style');
|
||||
if (strokeWidth != 0.0)
|
||||
result.write(' ${strokeWidth.toStringAsFixed(1)}');
|
||||
else
|
||||
result.write(' hairline');
|
||||
if (strokeCap != StrokeCap.butt) result.write(' $strokeCap');
|
||||
if (strokeJoin == StrokeJoin.miter) {
|
||||
if (strokeMiterLimit != _kStrokeMiterLimitDefault)
|
||||
result.write(
|
||||
' $strokeJoin up to ${strokeMiterLimit.toStringAsFixed(1)}');
|
||||
} else {
|
||||
result.write(' $strokeJoin');
|
||||
}
|
||||
semicolon = '; ';
|
||||
}
|
||||
if (isAntiAlias != true) {
|
||||
result.write('${semicolon}antialias off');
|
||||
semicolon = '; ';
|
||||
}
|
||||
if (color != const Color(_kColorDefault)) {
|
||||
if (color != null)
|
||||
result.write('$semicolon$color');
|
||||
else
|
||||
result.write('${semicolon}no color');
|
||||
semicolon = '; ';
|
||||
}
|
||||
if (blendMode.index != _kBlendModeDefault) {
|
||||
result.write('$semicolon$blendMode');
|
||||
semicolon = '; ';
|
||||
}
|
||||
if (colorFilter != null) {
|
||||
result.write('${semicolon}colorFilter: $colorFilter');
|
||||
semicolon = '; ';
|
||||
}
|
||||
if (maskFilter != null) {
|
||||
result.write('${semicolon}maskFilter: $maskFilter');
|
||||
semicolon = '; ';
|
||||
}
|
||||
if (filterQuality != FilterQuality.none) {
|
||||
result.write('${semicolon}filterQuality: $filterQuality');
|
||||
semicolon = '; ';
|
||||
}
|
||||
if (shader != null) {
|
||||
result.write('${semicolon}shader: $shader');
|
||||
semicolon = '; ';
|
||||
}
|
||||
if (invertColors) result.write('${semicolon}invert: $invertColors');
|
||||
result.write(')');
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
Paint toPaint() {
|
||||
Paint paint = Paint();
|
||||
if (this.isAntiAlias != true) paint.isAntiAlias = this.isAntiAlias;
|
||||
if (this.color != const Color(_kColorDefault)) paint.color = this.color;
|
||||
if (this.blendMode != BlendMode.srcOver) paint.blendMode = this.blendMode;
|
||||
if (this.style != PaintingStyle.fill) paint.style = this.style;
|
||||
if (this.strokeWidth != 0.0) paint.strokeWidth = this.strokeWidth;
|
||||
if (this.strokeCap != StrokeCap.butt) paint.strokeCap = this.strokeCap;
|
||||
if (this.strokeJoin != StrokeJoin.miter) paint.strokeJoin = this.strokeJoin;
|
||||
if (this.strokeMiterLimit != 4.0)
|
||||
paint.strokeMiterLimit = this.strokeMiterLimit;
|
||||
if (this.maskFilter != null) paint.maskFilter = this.maskFilter;
|
||||
if (this.filterQuality != FilterQuality.none)
|
||||
paint.filterQuality = this.filterQuality;
|
||||
if (this.shader != null) paint.shader = this.shader;
|
||||
if (this.colorFilter != null) paint.colorFilter = this.colorFilter;
|
||||
if (this.invertColors != false) paint.invertColors = this.invertColors;
|
||||
return paint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//flutter_bluetooth_serial_example
|
||||
//https://github.com/edufolly/flutter_bluetooth_serial/tree/master/example
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import './MainPage.dart';
|
||||
|
||||
void main() => runApp(new ExampleApplication());
|
||||
|
||||
class ExampleApplication extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(home: MainPage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.14.12"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_bluetooth_serial:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bluetooth_serial
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.6"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.8"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
scoped_model:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: scoped_model
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.3"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.15"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.6"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.8"
|
||||
sdks:
|
||||
dart: ">=2.7.0 <3.0.0"
|
||||
@@ -0,0 +1,78 @@
|
||||
name: androidbluetoothserialapp
|
||||
description: A new Flutter application.
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `pub publish`. This is preferred for private packages.
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
# The following defines the version and build number for your application.
|
||||
# A version number is three numbers separated by dots, like 1.2.43
|
||||
# followed by an optional build number separated by a +.
|
||||
# Both the version and the builder number may be overridden in flutter
|
||||
# build by specifying --build-name and --build-number, respectively.
|
||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.7.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^0.1.3
|
||||
flutter_bluetooth_serial: ^0.2.2
|
||||
scoped_model: ^1.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
After Width: | Height: | Size: 917 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter application.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="androidbluetoothserialapp">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="shortcut icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>androidbluetoothserialapp</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('flutter_service_worker.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "androidbluetoothserialapp",
|
||||
"short_name": "androidbluetoothserialapp",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter application.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||